diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6ba5eaa..7e31264d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,27 @@ jobs: if [ "$JDK_MAJOR_VERSION" == "7" ]; then export MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=128m"; fi ${{env.MVN_CMD}} verify -Possrh -Dgpg.skip=true + # ensure all of our files have the correct/updated license header + license-check: + runs-on: 'ubuntu-latest' + env: + MVN_CMD: ./mvnw --no-transfer-progress -B + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # avoid license plugin history warnings (plus it needs full history) + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '8' + cache: 'maven' + check-latest: true + - name: License Check + # This adds about 1 minute to any build, which is why we don't want to do it on every other build: + run: | + ${{env.MVN_CMD}} license:check + code-coverage: # (commented out for now - see the comments in 'Wait to start' below for why. Keeping this here as a placeholder # as it may be better to use instead of an artificial delay once we no longer need to build on JDK 7): @@ -107,4 +128,4 @@ jobs: ${{env.MVN_CMD}} -pl . clover:clover clover:check coveralls:report \ -DrepoToken="${{ secrets.GITHUB_TOKEN }}" \ -DserviceName=github \ - -DserviceBuildNumber="${{ env.GITHUB_RUN_ID }}" \ No newline at end of file + -DserviceBuildNumber="${{ env.GITHUB_RUN_ID }}" diff --git a/.gitignore b/.gitignore index bc177c84..436d64e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -*.class .DS_Store # Mobile Tools for Java (J2ME) diff --git a/.lift/config.toml b/.lift/config.toml index a833985e..22b51e7c 100644 --- a/.lift/config.toml +++ b/.lift/config.toml @@ -1,4 +1,21 @@ -ignoreRules = ["MissingOverride"] -ignoreFiles = ''' +# +# Copyright © 2022 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. +# + +ignoreRules = ["MissingOverride", "MissingSummary", "InconsistentCapitalization", "JavaUtilDate", "TypeParameterUnusedInFormals", "JavaLangClash", "InlineFormatString"] +ignoreFiles = """ +impl/** **/test/** -''' +""" diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d016ce..077b6bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,215 @@ ### JJWT_RELEASE_VERSION -* Adds a simplified "starter" jar that automatically pulls in `jjwt-api`, `jjwt-impl` and `jjwt-jackson`, useful when - upgrading from the older `io.jsonwebtoken:jjwt:*` to the project's current flexible module structure. +This is a big release! JJWT now fully supports Encrypted JSON Web Tokens (JWE) and JSON Web Keys (JWK)! See the +sections below enumerating all new features as well as important notes on breaking changes or backwards-incompatible +changes made in preparation for the upcoming 1.0 release. + +#### Simplified Starter Jar + +Those upgrading to new modular JJWT versions from old single-jar versions will transparently obtain everything +they need in their Maven, Gradle or Android projects. + +JJWT's early releases had one and only one .jar: `jjwt.jar`. Later releases moved to a modular design with 'api' and +'impl' jars including 'plugin' jars for Jackson, GSON, org.json, etc. Some users upgrading from the earlier single +jar to JJWT's later versions have been frustrated by being forced to learn how to configure the more modular .jars. + +This release re-introduces the `jjwt.jar` artifact again, but this time it is simply an empty .jar with Maven +metadata that will automatically transitively download the following into a project, retaining the old single-jar +behavior: +* `jjwt-api.jar` +* `jjwt-impl.jar` +* `jjwt-jackson.jar` + +Naturally, developers are still encouraged to configure the modular .jars as described in JJWT's documentation for +greater control and to enable their preferred JSON parser, but this stop-gap should help those unaware when upgrading. + +#### JSON Web Encryption (JWE) Support! + +This has been a long-awaited feature for JJWT, years in the making, and it is quite extensive - so many encryption +algorithms and key management algorithms are defined by the JWA specification, and new API concepts had to be +introduced for all of them, as well as extensive testing with RFC-defined test vectors. The wait is over! +All JWA-defined encryption algorithms and key management algorithms are fully implemented and supported and +available immediately. For example: + +```java +AeadAlgorithm enc = Jwts.ENC.A256GCM; +SecretKey key = enc.keyBuilder().build(); +String compact = Jwts.builder().setSubject("Joe").encryptWith(key, enc).compact(); + +Jwe jwe = Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(compact); +``` + +Many other RSA and Elliptic Curve examples are in the full README documentation. + +#### JSON Web Key (JWK) Support! + +Representing cryptographic keys - SecretKeys, RSA Public and Private Keys, Elliptic Curve Public and +Private keys - as fully encoded JSON objects according to the JWK specification - is now fully implemented and +supported. The new `Jwks` utility class exists to create JWK builders and parsers as desired. For example: + +```java +SecretKey key = Jwts.SIG.HS256.keyBuilder().build(); +SecretJwk jwk = Jwks.builder().forKey(key).build(); +assert key.equals(jwk.toKey()); + +// or if receiving a JWK string: +Jwk parsedJwk = Jwks.parser().build().parse(jwkString); +assert jwk.equals(parsedJwk); +assert key.equals(parsedJwk.toKey()); +``` + +Many JJWT users won't need to use JWKs explicitly, but some JWA Key Management Algorithms (and lots of RFC test +vectors) utilize JWKs when transmitting JWEs. As this was required by JWE, it is now implemented in full for +JWE use as well as general-purpose JWK support. + +#### Better PKCS11 and Hardware Security Module (HSM) support + +Previous versions of JJWT enforced that Private Keys implemented the `RSAKey` and `ECKey` interfaces to enforce key +length requirements. With this release, JJWT will still perform those checks when those data types are available, +but if not, as is common with keys from PKCS11 and HSM KeyStores, JJWT will still allow those Keys to be used, +expecting the underlying Security Provider to enforce any key requirements. This should reduce or eliminate any +custom code previously written to extend JJWT to use keys from those KeyStores or Providers. + +#### Custom Signature Algorithms + +The `io.jsonwebtoken.SignatureAlgorithm` enum has been deprecated in favor of new +`io.jsonwebtoken.security.SecureDigestAlgorithm`, `io.jsonwebtoken.security.MacAlgorithm`, and +`io.jsonwebtoken.security.SignatureAlgorithm` interfaces to allow custom algorithm implementations. The new +`SIG` constant in the `Jwts` helper class is a registry of all standard JWS algorithms as expected, exactly like the +old enum. This change was made because enums are a static concept by design and cannot +support custom values: those who wanted to use custom signature algorithms could not do so until now. The new +interface now allows anyone to plug in and support custom algorithms with JJWT as desired. + +#### KeyBuilder and KeyPairBuilder + +Because the `io.jsonwebtoken.security.Keys#secretKeyFor` and `io.jsonwebtoken.security.Keys#keyPairFor` methods +accepted the now-deprecated `io.jsonwebtoken.SignatureAlgorithm` enum, they have also been deprecated in favor of +calling new `keyBuilder()` or `keyPairBuilder()` methods on `MacAlgorithm` and `SignatureAlgorithm` instances directly. +For example: + +```java +SecretKey key = Jwts.SIG.HS256.keyBuilder().build(); +KeyPair pair = Jwts.SIG.RS256.keyPairBuilder().build(); +``` + +The builders allow for customization of the JCA `Provider` and `SecureRandom` during Key or KeyPair generation if desired, whereas +the old enum-based static utility methods did not. + +#### Preparation for 1.0 + +Now that the JWE and JWK specifications are implemented, only a few things remain for JJWT to be considered at +version 1.0. We have been waiting to apply the 1.0 release version number until the entire set of JWT specifications +are fully supported and we drop JDK 7 support (to allow users to use JDK 8 APIs). To that end, we have had to +deprecate some concepts, or in some rare cases, completely break backwards compatibility to ensure the transition to +1.0 (and JDK 8 APIs) are possible. Any backwards-incompatible changes are listed in the next section below. + +#### Backwards Compatibility Breaking Changes, Warnings and Deprecations + +* `io.jsonwebtoken.Jwts`'s `header(Map)`, `jwsHeader()` and `jwsHeader(Map)` methods have been deprecated in favor + of the new `header()` builder-based method to support method chaining and dynamic Header type creation. + + +* `io.jsonwebtoken.Jwt`'s `getBody()` method has been deprecated in favor of a new `getPayload()` method to + reflect correct JWT specification nomenclature/taxonomy. + + +* `io.jsonwebtoken.CompressionCodec` now inherits a new `io.jsonwebtoken.Identifiable` interface and its `getId()` + method is preferred over the now-deprecated `getAlgorithmName()` method. This is to guarantee API congruence with + all other JWT-identifiable algorithm names that can be set as a header value. + + +* `io.jsonwebtoken.Header` has been changed to accept a type-parameter for sub-type method return values, i.e. + `io.jsonwebtoken.Header` and a new `io.jsonwebtoken.UnprotectedHeader` interface has been + introduced to represent the concrete type of header without integrity protection. This new `UnprotectedHeader` is + to be used where the previous generic `Header` (non-`JweHeader` and non-`JwsHeader`) interface was used. + + +* Accordingly, the `Jwts.header()` and `Jwts.header(Map)` now return instances of `UnprotectedHeader` instead + of just `Header`. + + +#### Breaking Changes + +* **JWTs that do not contain JSON Claims now have a payload type of `byte[]` instead of `String`** (that is, + `Jwt` instead of `Jwt`). This is because JWTs, especially when used with the + `cty` (Content Type) header, are capable of handling _any_ type of payload, not just Strings. The previous JJWT + releases didn't account for this, and now the API accurately reflects the JWT RFC specification payload + capabilities. Additionally, the name of `plaintext` has been changed to `content` in method names and JavaDoc to + reflect this taxonomy. This change has impacted the following JJWT APIs: + + * The `JwtBuilder`'s `setPayload(String)` method has been deprecated in favor of two new methods: + + * `setContent(byte[])`, and + * `setContent(byte[], String contentType)` + + These new methods allow any kind of content + within a JWT, not just Strings. The existing `setPayload(String)` method implementation has been changed to + delegate to this new `setContent(byte[])` method with the argument's UTF-8 bytes, for example + `setContent(payloadString.getBytes(StandardCharsets.UTF_8))`. + + * The `JwtParser`'s `Jwt parsePlaintextJwt(String plaintextJwt)` and + `Jws parsePlaintextJws(String plaintextJws)` methods have been changed to + `Jwt parseContentJwt(String plaintextJwt)` and + `Jws parseContentJws(String plaintextJws)` respectively. + + * `JwtHandler`'s `onPlaintextJwt(String)` and `onPlaintextJws(String)` methods have been changed to + `onContentJwt(byte[])` and `onContentJws(byte[])` respectively. + + * `io.jsonwebtoken.JwtHandlerAdapter` has been changed to reflect the above-mentioned name and `String`-to-`byte[]` + argument changes, as well adding the `abstract` modifier. This class was never intended + to be instantiated directly, and is provided for subclassing only. The missing modifier has been added to ensure + the class is used as it had always been intended. + + * `io.jsonwebtoken.SigningKeyResolver`'s `resolveSigningKey(JwsHeader, String)` method has been changed to + `resolveSigningKey(JwsHeader, byte[])`. + +* `io.jsonwebtoken.Jwts`'s `parser()` method deprecated 4 years ago has been renamed to `legacyParser()` to + allow an updated `parser()` method to return a `JwtParserBuilder` instead of a direct `JwtParser` instance. + This `legacyParser()` method will be removed entirely for the 1.0 release - please change your code to use the + updated `parser()` method that returns a builder as soon as possible. + +* `io.jsonwebtoken.Jwts`'s `header()` method has been renamed to `unprotectedHeader()` to allow a newer/updated + `header()` method to return a `DynamicHeaderBuilder` instead of a direct `Header` instance. This new method / + return value is the recommended approach for building headers, as it will dynamically create an `UnprotectedHeader`, + `JwsHeader` or `JweHeader` automatically based on builder state. + +* `io.jsonwebtoken.Jwts`'s `headerBuilder()` method has been renamed to `header()` and returns a + `DynamicHeaderBuilder` instead of a direct `Header` instance. This builder method is the recommended approach + for building headers in the future, as it will dynamically create an `UnprotectedHeader`, `JwsHeader` or `JweHeader` + automatically based on builder state. + +* `io.jsonwebtoken.Jwts`'s `header()` method now returns a `DynamicHeaderBuilder` instead of a + direct `Header` instance. This new method / return value is the recommended approach for building headers + in the future, as it will dynamically create an `UnprotectedHeader`, `JwsHeader` or `JweHeader` automatically + based on builder state. + +* Prior to this release, if there was a serialization problem when serializing the JWT Header, an `IllegalStateException` + was thrown. If there was a problem when serializing the JWT claims, an `IllegalArgumentException` was + thrown. This has been changed up to ensure consistency: any serialization error with either headers or claims + will now throw a `io.jsonwebtoken.io.SerializationException`. + + +* Parsing of unsecured JWTs (`alg` header of `none`) are now disabled by default as mandated by + [RFC 7518, Section 3.6](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.6). If you require parsing of + unsecured JWTs, you must call the `enableUnsecuredJws` method on the `JwtParserBuilder`, but note the security + implications mentioned in that method's JavaDoc before doing so. + + +* `io.jsonwebtoken.gson.io.GsonSerializer` now requires `Gson` instances that have a registered + `GsonSupplierSerializer` type adapter, for example: + ```java + new GsonBuilder() + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); + ``` + This is to ensure JWKs have `toString()` and application log safety (do not print secure material), but still + serialize to JSON correctly. + +* `io.jsonwebtoken.InvalidClaimException` and it's two subclasses (`IncorrectClaimException` and `MissingClaimException`) + were previously mutable, allowing the corresponding claim name and claim value to be set on the exception after + creation. These should have always been immutable without those setters (just getters), and this was a previous + implementation oversight. This release has ensured they are immutable without the setters. ### 0.11.5 diff --git a/README.md b/README.md index e864b0e7..387114e4 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,17 @@ [![Coverage Status](https://coveralls.io/repos/github/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/github/jwtk/jjwt?branch=master) [![Gitter](https://badges.gitter.im/jwtk/jjwt.svg)](https://gitter.im/jwtk/jjwt?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -## Java JWT: JSON Web Token for Java and Android +# Java JWT: JSON Web Token for Java and Android JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM and Android. -JJWT is a pure Java implementation based -exclusively on the [JWT](https://tools.ietf.org/html/rfc7519), +JJWT is a pure Java implementation based exclusively on the [JWT](https://tools.ietf.org/html/rfc7519), [JWS](https://tools.ietf.org/html/rfc7515), [JWE](https://tools.ietf.org/html/rfc7516), -[JWK](https://tools.ietf.org/html/rfc7517) and [JWA](https://tools.ietf.org/html/rfc7518) RFC specifications and +[JWA](https://tools.ietf.org/html/rfc7518), [JWK](https://tools.ietf.org/html/rfc7517), +[Octet JWK](https://www.rfc-editor.org/rfc/rfc8037), +[JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html), and +[JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) RFC specifications and open source under the terms of the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). The library was created by [Les Hazlewood](https://github.com/lhazlewood) @@ -31,6 +33,9 @@ enforcement. * [Pull Requests](#contributing-pull-requests) * [Help Wanted](#contributing-help-wanted) * [What is a JSON Web Token?](#overview) + * [JWT Example](#overview-example-jwt) + * [JWS Example](#overview-example-jws) + * [JWE Example](#overview-example-jwe) * [Installation](#install) * [JDK Projects](#install-jdk) * [Maven](#install-jdk-maven) @@ -38,9 +43,34 @@ enforcement. * [Android Projects](#install-android) * [Dependencies](#install-android-dependencies) * [Proguard Exclusions](#install-android-proguard) + * [Bouncy Castle](#install-android-bc) * [Understanding JJWT Dependencies](#install-understandingdependencies) * [Quickstart](#quickstart) +* [Create a JWT](#jwt-create) + * [Header](#jwt-header) + * [Header Builder](#jwt-header-builder) + * [Header Parameters](#jwt-header-params) + * [Header Map](#jwt-header-map) + * [Payload](#jwt-payload) + * [Arbitrary Content](#jwt-content) + * [Claims](#jwt-claims) + * [Standard Claims](#jwt-claims-standard) + * [Custom Claims](#jwt-claims-custom) + * [Claims Instance](#jwt-claims-instance) + * [Claims Map](#jwt-claims-map) + * [Compression](#jwt-compression) +* [Read a JWT](#jwt-read) + * [Static Parsing Key](#jwt-read-key) + * [Dynamic Parsing Key Lookup](#key-locator) + * [Custom Key Locator](#key-locator-custom) + * [Key Locator Strategy](#key-locator-strategy) + * [Key Locator Return Values](#key-locator-retvals) + * [Claim Assertions](#jwt-read-claims) + * [Accounting for Clock Skew](#jwt-read-clock) + * [Custom Clock Support](#jwt-read-clock-custom) + * [Decompression](#jwt-read-decompression) * [Signed JWTs](#jws) + * [Standard Signature Algorithms](#jws-alg) * [Signature Algorithm Keys](#jws-key) * [HMAC-SHA](#jws-key-hmacsha) * [RSA](#jws-key-rsa) @@ -49,28 +79,46 @@ enforcement. * [Secret Keys](#jws-key-create-secret) * [Asymetric Keys](#jws-key-create-asym) * [Create a JWS](#jws-create) - * [Header](#jws-create-header) - * [Instance](#jws-create-header-instance) - * [Map](#jws-create-header-map) - * [Claims](#jws-create-claims) - * [Standard Claims](#jws-create-claims-standard) - * [Custom Claims](#jws-create-claims-custom) - * [Claims Instance](#jws-create-claims-instance) - * [Claims Map](#jws-create-claims-map) * [Signing Key](#jws-create-key) * [SecretKey Formats](#jws-create-key-secret) * [Signature Algorithm Override](#jws-create-key-algoverride) * [Compression](#jws-create-compression) * [Read a JWS](#jws-read) * [Verification Key](#jws-read-key) - * [Find the Verification Key at Runtime](#jws-read-key-resolver) - * [Claims Assertions](#jws-read-claims) - * [Accounting for Clock Skew](#jws-read-clock) - * [Custom Clock](#jws-read-clock-custom) + * [Verification Key Locator](#jws-read-key-locator) * [Decompression](#jws-read-decompression) +* [Encrypted JWTs](#jwe) + * [JWE Encryption Algorithms](#jwe-enc) + * [JWE Symmetric Encryption](#jwe-enc-symmetric) + * [JWE Key Management Algorithms](#jwe-alg) + * [JWE Standard Key Management Algorithms](#jwe-alg-standard) + * [JWE RSA Key Encryption](#jwe-alg-rsa) + * [JWE AES Key Encryption](#jwe-alg-aes) + * [JWE Direct Key Encryption](#jwe-alg-dir) + * [JWE Password-based Key Encryption](#jwe-alg-pbes2) + * [JWE Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement](#jwe-alg-ecdhes) + * [Create a JWE](#jwe-create) + * [JWE Compression](#jwe-compression) + * [Read a JWE](#jwe-read) + * [JWE Decryption Key](#jwe-read-key) + * [JWE Decryption Key Locator](#jwe-key-locator) + * [JWE Decompression](#jwe-read-decompression) +* [JSON Web Keys (JWKs)](#jwk) + * [Create a JWK](#jwk-create) + * [Read a JWK](#jwk-read) + * [PrivateKey JWKs](#jwk-private) + * [Private JWK `PublicKey`](#jwk-private-public) + * [Private JWK from `KeyPair`](#jwk-private-keypair) + * [Private JWK Public Conversion](#jwk-private-topub) + * [JWK Thumbprints](#jwk-thumbprint) + * [JWK Thumbprint as Key ID](jwk-thumbprint-kid) + * [JWK Thumbprint URI](#jwk-thumbprint-uri) + * [JWK Security Considerations](#jwk-security) + * [JWK `toString()` Safety](#jwk-tostring) * [Compression](#compression) * [Custom Compression Codec](#compression-custom) + * [Custom Compression Codec Locator](#compression-custom-locator) * [JSON Processor](#json) * [Custom JSON Processor](#json-custom) * [Jackson ObjectMapper](#json-jackson) @@ -81,6 +129,22 @@ enforcement. * [Base64 is not Encryption](#base64-not-encryption) * [Changing Base64 Characters](#base64-changing-characters) * [Custom Base64 Codec](#base64-custom) +* [Examples](#examples) + * [JWS Signed with HMAC](#example-jws-hs) + * [JWS Signed with RSA](#example-jws-rsa) + * [JWS Signed with ECDSA](#example-jws-ecdsa) + * [JWE Encrypted Directly with a SecretKey](#example-jwe-dir) + * [JWE Encrypted with RSA](#example-jwe-rsa) + * [JWE Encrypted with AES Key Wrap](#example-jwe-aeskw) + * [JWE Encrypted with ECDH-ES](#example-jwe-ecdhes) + * [JWE Encrypted with a Password](#example-jwe-password) + * [SecretKey JWK](#example-jwk-secret) + * [RSA Public JWK](#example-jwk-rsapub) + * [RSA Private JWK](#example-jwk-rsapriv) + * [Elliptic Curve Public JWK](#example-jwk-ecpub) + * [Elliptic Curve Private JWK](#example-jwk-ecpriv) + * [Edwards Elliptic Curve Public JWK](#example-jwk-edpub) + * [Edwards Elliptic Curve Private JWK](#example-jwk-edpriv) ## Features @@ -88,29 +152,92 @@ enforcement. * Fully functional on all JDKs and Android * Automatic security best practices and assertions * Easy to learn and read API - * Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE auto-completion to write code quickly + * Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE + auto-completion to write code quickly * Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors - * Stable implementation with enforced 100% test code coverage. Literally every single method, statement and - conditional branch variant in the entire codebase is tested and required to pass on every build. + * Stable implementation with over 1,200+ tests and enforced 100% test code coverage. Every single method, statement + and conditional branch variant in the entire codebase is tested and required to pass on every build. * Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms: - * HS256: HMAC using SHA-256 - * HS384: HMAC using SHA-384 - * HS512: HMAC using SHA-512 - * ES256: ECDSA using P-256 and SHA-256 - * ES384: ECDSA using P-384 and SHA-384 - * ES512: ECDSA using P-521 and SHA-512 - * RS256: RSASSA-PKCS-v1_5 using SHA-256 - * RS384: RSASSA-PKCS-v1_5 using SHA-384 - * RS512: RSASSA-PKCS-v1_5 using SHA-512 - * PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-2561 - * PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-3841 - * PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-5121 - - 1. Requires JDK 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + + | Identifier | Signature Algorithm | + |------------|-------------------------------------------------------------------| + | `HS256` | HMAC using SHA-256 | + | `HS384` | HMAC using SHA-384 | + | `HS512` | HMAC using SHA-512 | + | `ES256` | ECDSA using P-256 and SHA-256 | + | `ES384` | ECDSA using P-384 and SHA-384 | + | `ES512` | ECDSA using P-521 and SHA-512 | + | `RS256` | RSASSA-PKCS-v1_5 using SHA-256 | + | `RS384` | RSASSA-PKCS-v1_5 using SHA-384 | + | `RS512` | RSASSA-PKCS-v1_5 using SHA-512 | + | `PS256` | RSASSA-PSS using SHA-256 and MGF1 with SHA-2561 | + | `PS384` | RSASSA-PSS using SHA-384 and MGF1 with SHA-3841 | + | `PS512` | RSASSA-PSS using SHA-512 and MGF1 with SHA-5121 | + | `EdDSA` | Edwards-curve Digital Signature Algorithm2 | + + 1. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + + 2. Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + + * Creating, parsing and decrypting encrypted compact JWTs (aka JWEs) with all standard JWE encryption algorithms: + + | Identifier | Encryption Algorithm | + |----------------------------------|--------------------------------------------------------------------------------------------------------------------------| + | A128CBC‑HS256 | [AES_128_CBC_HMAC_SHA_256](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) authenticated encryption algorithm | + | `A192CBC-HS384` | [AES_192_CBC_HMAC_SHA_384](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4) authenticated encryption algorithm | + | `A256CBC-HS512` | [AES_256_CBC_HMAC_SHA_512](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5) authenticated encryption algorithm | + | `A128GCM` | AES GCM using 128-bit key3 | + | `A192GCM` | AES GCM using 192-bit key3 | + | `A256GCM` | AES GCM using 256-bit key3 | + + 3. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + + * All Key Management Algorithms for obtaining JWE encryption and decryption keys: + + | Identifier | Key Management Algorithm | + |----------------------|-------------------------------------------------------------------------------| + | `RSA1_5` | RSAES-PKCS1-v1_5 | + | `RSA-OAEP` | RSAES OAEP using default parameters | + | `RSA-OAEP-256` | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | + | `A128KW` | AES Key Wrap with default initial value using 128-bit key | + | `A192KW` | AES Key Wrap with default initial value using 192-bit key | + | `A256KW` | AES Key Wrap with default initial value using 256-bit key | + | `dir` | Direct use of a shared symmetric key as the CEK | + | `ECDH-ES` | Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF | + | `ECDH-ES+A128KW` | ECDH-ES using Concat KDF and CEK wrapped with "A128KW" | + | `ECDH-ES+A192KW` | ECDH-ES using Concat KDF and CEK wrapped with "A192KW" | + | `ECDH-ES+A256KW` | ECDH-ES using Concat KDF and CEK wrapped with "A256KW" | + | `A128GCMKW` | Key wrapping with AES GCM using 128-bit key4 | + | `A192GCMKW` | Key wrapping with AES GCM using 192-bit key4 | + | `A256GCMKW` | Key wrapping with AES GCM using 256-bit key4 | + | `PBES2-HS256+A128KW` | PBES2 with HMAC SHA-256 and "A128KW" wrapping4 | + | `PBES2-HS384+A192KW` | PBES2 with HMAC SHA-384 and "A192KW" wrapping4 | + | PBES2‑HS512+A256KW | PBES2 with HMAC SHA-512 and "A256KW" wrapping4 | + + 4. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + + * Creating, parsing and verifying JSON Web Keys (JWKs) in all standard JWA key formats using native Java `Key` types: + + | JWK Key Format | Java `Key` Type | JJWT `Jwk` Type | + |----------------------------|------------------------------------|-------------------| + | Symmetric Key | `SecretKey` | `SecretJwk` | + | Elliptic Curve Public Key | `ECPublicKey` | `EcPublicJwk` | + | Elliptic Curve Private Key | `ECPrivateKey` | `EcPrivateJwk` | + | RSA Public Key | `RSAPublicKey` | `RsaPublicJwk` | + | RSA Private Key | `RSAPrivateKey` | `RsaPrivateJwk` | + | XDH Private Key | `XECPublicKey`5 | `OctetPublicJwk` | + | XDH Private Key | `XECPrivateKey`5 | `OctetPrivateJwk` | + | EdDSA Public Key | `EdECPublicKey`6 | `OctetPublicJwk` | + | EdDSA Private Key | `EdECPublicKey`6 | `OctetPrivateJwk` | + + 5. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + + 6. Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + * Convenience enhancements beyond the specification such as - * Body compression for any large JWT, not just JWEs + * Payload compression for any large JWT, not just JWEs * Claims assertions (requiring specific values) - * Claim POJO marshaling and unmarshaling when using a compatible JSON parser (e.g. Jackson) + * Claim POJO marshaling and unmarshalling when using a compatible JSON parser (e.g. Jackson) * Secure Key generation based on desired JWA algorithms * and more... @@ -118,9 +245,8 @@ enforcement. ### Currently Unsupported Features * [Non-compact](https://tools.ietf.org/html/rfc7515#section-7.2) serialization and parsing. -* JWE (Encryption for JWT) -These features will be implemented in a future release. Community contributions are welcome! +This feature may be implemented in a future release. Community contributions are welcome! ## Community @@ -129,7 +255,8 @@ These features will be implemented in a future release. Community contributions ### Getting Help If you have trouble using JJWT, please first read the documentation on this page before asking questions. We try -very hard to ensure JJWT's documentation is robust, categorized with a table of contents, and up to date for each release. +very hard to ensure JJWT's documentation is robust, categorized with a table of contents, and up to date for each +release. #### Questions @@ -152,7 +279,8 @@ usability question, instead please [ask your question here](https://stackoverflow.com/questions/ask?tags=jjwt&guided=false), or try Slack or Gittr as described above. -**If a GitHub Issue is created that does not represent actionable work for JJWT's codebase, it will be promptly closed.** +**If a GitHub Issue is created that does not represent actionable work for JJWT's codebase, it will be promptly +closed.** #### Bugs and Feature Requests @@ -176,7 +304,7 @@ However, if you want or feel the need to change JJWT's functionality or core cod without [creating a new JJWT issue](https://github.com/jwtk/jjwt/issues/new) and discussing your desired changes **first**, _before you start working on it_. -It would be a shame to reject your earnest and genuinely appreciated pull request if it might not align with the +It would be a shame to reject your earnest and genuinely-appreciated pull request if it might not align with the project's goals, design expectations or planned functionality. We've sadly had to reject large PRs in the past because they were out of sync with project or design expectations - all because the PR author didn't first check in with the team first before working on a solution. @@ -198,51 +326,198 @@ to discuss or ask questions first if you're not sure. :) ## What is a JSON Web Token? -Don't know what a JSON Web Token is? Read on. Otherwise, jump on down to the [Installation](#Installation) section. +JSON Web Token (JWT) is a _general-purpose_ text-based messaging format for transmitting information in a +compact and secure way. Contrary to popular belief, JWT is not just useful for sending and receiving identity tokens +on the web - even if that is the most common use case. JWTs can be used as messages for _any_ type of data. -JWT is a means of transmitting information between two parties in a compact, verifiable form. +A JWT in its simplest form contains two parts: -The bits of information encoded in the body of a JWT are called `claims`. The expanded form of the JWT is in a JSON format, so each `claim` is a key in the JSON object. - -JWTs can be cryptographically signed (making it a [JWS](https://tools.ietf.org/html/rfc7515)) or encrypted (making it a [JWE](https://tools.ietf.org/html/rfc7516)). + 1. The primary data within the JWT, called the `payload`, and + 2. A JSON `Object` with name/value pairs that represent metadata about the `payload` and the + message itself, called the `header`. -This adds a powerful layer of verifiability to the user of JWTs. The receiver has a high degree of confidence that the JWT has not been tampered with by verifying the signature, for instance. +A JWT `payload` can be absolutely anything at all - anything that can be represented as a byte array, such as Strings, +images, documents, etc. -The compact representation of a signed JWT is a string that has three parts, each separated by a `.`: +But because a JWT `header` is a JSON `Object`, it would make sense that a JWT `payload` could also be a JSON +`Object` as well. In many cases, developers like the `payload` to be JSON that +represents data about a user or computer or similar identity concept. When used this way, the `payload` is called a +JSON `Claims` object, and each name/value pair within that object is called a `claim` - each piece of information +within 'claims' something about an identity. + +And while it is useful to 'claim' something about an identity, really anyone can do that. What's important is that you +_trust_ the claims by verifying they come from a person or computer you trust. + +A nice feature of JWTs is that they can be secured in various ways. A JWT can be cryptographically signed (making it +what we call a [JWS](https://tools.ietf.org/html/rfc7515)) or encrypted (making it a +[JWE](https://tools.ietf.org/html/rfc7516)). This adds a powerful layer of verifiability to the JWT - a +JWS or JWE recipient can have a high degree of confidence it comes from someone they trust +by verifying a signature or decrypting it. It is this feature of verifiability that makes JWT a good choice +for sending and receiving secure information, like identity claims. + +Finally, JSON with whitespace for human readability is nice, but it doesn't make for a very efficient message +format. Therefore, JWTs can be _compacted_ (and even compressed) to a minimal representation - basically +Base64URL-encoded strings - so they can be transmitted around the web more efficiently, such as in HTTP headers or URLs. + + +### JWT Example + +Once you have a `payload` and `header`, how are they compacted for web transmission, and what does the final JWT +actually look like? Let's walk through a simplified version of the process with some pseudocode: + +1. Assume we have a JWT with a JSON `header` and a simple text message payload: + + **header** + ``` + { + "alg": "none" + } + ``` + + **payload** + ``` + The true sign of intelligence is not knowledge but imagination. + ``` + +2. Remove all unnecessary whitespace in the JSON: + + ```groovy + String header = '{"alg":"none"}' + String payload = 'The true sign of intelligence is not knowledge but imagination.' + ``` + +3. Get the UTF-8 bytes and Base64URL-encode each: + + ```groovy + String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) + String encodedPayload = base64URLEncode( payload.getBytes("UTF-8") ) + ``` + +4. Join the encoded header and claims with period ('.') characters: + + ```groovy + String compact = encodedHeader + '.' + encodedPayload + '.' + ``` + +The final concatenated `compact` JWT String looks like this: ``` -eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY +eyJhbGciOiJub25lIn0.VGhlIHRydWUgc2lnbiBvZiBpbnRlbGxpZ2VuY2UgaXMgbm90IGtub3dsZWRnZSBidXQgaW1hZ2luYXRpb24u. ``` -Each part is [Base64URL](https://en.wikipedia.org/wiki/Base64)-encoded. The first part is the header, which at a -minimum needs to specify the algorithm used to sign the JWT. The second part is the body. This part has all -the claims of this JWT encoded in it. The final part is the signature. It's computed by passing a combination of -the header and body through the algorithm specified in the header. - -If you pass the first two parts through a base 64 url decoder, you'll get the following (formatting added for -clarity): +This is called an 'unprotected' JWT because no security was involved - no digital signatures or encryption to +'protect' the JWT to ensure it cannot be changed by 3rd parties. + +If we wanted to digitally sign the compact form so that we could at least guarantee that no-one changes the data +without us detecting it, we'd have to perform a few more steps, shown next. + + +### JWS Example + +Instead of a plain text payload, the next example will use probably the most common type of payload - a JSON claims +`Object` containing information about a particular identity. We'll also digitally sign the JWT to ensure it +cannot be changed by a 3rd party without us knowing. + +1. Assume we have a JSON `header` and a claims `payload`: + + **header** + ```json + { + "alg": "HS256" + } + ``` + + **payload** + ```json + { + "sub": "Joe" + } + ``` + + In this case, the `header` indicates that the `HS256` (HMAC using SHA-256) algorithm will be used to cryptographically sign + the JWT. Also, the `payload` JSON object has a single claim, `sub` with value `Joe`. + + There are a number of standard claims, called [Registered Claims](https://tools.ietf.org/html/rfc7519#section-4.1), + in the specification and `sub` (for 'Subject') is one of them. + +2. Remove all unnecessary whitespace in both JSON objects: + + ```groovy + String header = '{"alg":"HS256"}' + String claims = '{"sub":"Joe"}' + ``` + +3. Get their UTF-8 bytes and Base64URL-encode each: + + ```groovy + String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) + String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") ) + ``` + +4. Concatenate the encoded header and claims with a period character '.' delimiter: + + ```groovy + String concatenated = encodedHeader + '.' + encodedClaims + ``` + +5. Use a sufficiently-strong cryptographic secret or private key, along with a signing algorithm of your choice + (we'll use HMAC-SHA-256 here), and sign the concatenated string: + + ```groovy + SecretKey key = getMySecretKey() + byte[] signature = hmacSha256( concatenated, key ) + ``` + +6. Because signatures are always byte arrays, Base64URL-encode the signature and join it to the `concatenated` string + with a period character '.' delimiter: + + ```groovy + String compact = concatenated + '.' + base64URLEncode( signature ) + ``` + +And there you have it, the final `compact` String looks like this: -`header` ``` -{ - "alg": "HS256" -} +eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4 ``` -`body` +This is called a 'JWS' - short for _signed_ JWT. + +Of course, no one would want to do this manually in code, and worse, if you get anything wrong, you could introduce +serious security problems and weaknesses. As a result, JJWT was created to handle all of this for you: JJWT completely +automates both the creation of JWSs and the parsing and verification of JWSs for you. + + +### JWE Example + +So far we have seen an unprotected JWT and a cryptographically signed JWT (called a 'JWS'). One of the things +that is inherent to both of these two is that all the information within them can be seen by anyone - all the data in +both the header and the payload is publicly visible. JWS just ensures the data hasn't been changed by anyone - +it doesn't prevent anyone from seeing it. Many times, this is just fine because the data within them is not +sensitive information. + +But what if you needed to represent information in a JWT that _is_ considered sensitive information - maybe someone's +postal address or social security number or bank account number? + +In these cases, we'd want a fully-encrypted JWT, called a 'JWE' for short. A JWE uses cryptography to ensure that the +payload remains fully encrypted _and_ authenticated so unauthorized parties cannot see data within, nor change the data +without being detected. Specifically, the JWE specification requires that +[Authenticated Encryption with Associated Data](https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)) +algorithms are used to fully encrypt and protect data. + +A full overview of AEAD algorithms are out of scope for this documentation, but here's an example of a final compact +JWE that utilizes these algorithms (line breaks are for readability only): + ``` -{ - "sub": "Joe" -} +eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0. +6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ. +AxY8DCtDaGlsbGljb3RoZQ. +KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY. +U0m_YmjN04DJvceFICbCVQ ``` -In this case, the information we have is that the HMAC using SHA-256 algorithm was used to sign the JWT. And, the -body has a single claim, `sub` with value `Joe`. - -There are a number of standard claims, called [Registered Claims](https://tools.ietf.org/html/rfc7519#section-4.1), -in the specification and `sub` (for subject) is one of them. - -To compute the signature, you need a secret key to sign it. We'll cover keys and algorithms later. +Next we'll cover how to install JJWT in your project, and then we'll see how to use JJWT's nice fluent API instead +of risky string manipulation to quickly and safely build JWTs, JWSs, and JWEs. ## Installation @@ -264,22 +539,25 @@ If you're building a (non-Android) JDK project, you will want to define the foll io.jsonwebtoken jjwt-api - 0.11.5 + JJWT_RELEASE_VERSION io.jsonwebtoken jjwt-impl - 0.11.5 + JJWT_RELEASE_VERSION runtime io.jsonwebtoken jjwt-jackson - 0.11.5 + JJWT_RELEASE_VERSION runtime - +### JWT Header + +A JWT header is a JSON `Object` that provides metadata about the contents, format, and any cryptographic operations +relevant to the JWT `payload`. JJWT provides a number of ways of setting the entire header and/or multiple individual +header parameters (name/value pairs). + + +#### Header Builder + +The easiest and recommended way to set one or more JWT header parameters (name/value pairs) is to call +`JwtBuilder` `setHeader` with `Jwts.headerBuilder()`. For example: ```java -String jws = Jwts.builder() +String jwt = Jwts.builder() + + .setHeader(Jwts.headerBuilder() // <---- + .setKeyId("aKeyId") + .setX509Url(aUri) + .put("someName", "anyValue") + .putAll(anotherMap) + // ... etc ... + ) + // ... etc ... + .compact(); +``` + +In addition to type-safe setter methods, `Jwts.headerBuilder()` can also support arbitrary name/value pairs via +`put` and `putAll` as shown above. It can also support automatically calculating +X.509 thumbprints and other builder-style benefits that the other `JwtBuilder` `setHeader`* methods do not support. +For this reason, `Jwts.headerBuilder()` is the recommended way to set a JWT header and is preferred over the other +approaches listed next. + +> **Note** +> +> **Automatic Headers**: You do not need to set the `alg`, `enc` or `zip` headers - JJWT will set them automatically +> as needed. + + +#### Header Parameters + +Another way of setting header parameters is to call `JwtBuilder` `setHeaderParam` one or more times as needed: + +```java +String jwt = Jwts.builder() .setHeaderParam("kid", "myKeyId") @@ -678,47 +836,28 @@ String jws = Jwts.builder() ``` -Each time `setHeaderParam` is called, it simply appends the key-value pair to an internal `Header` instance, +Each time `setHeaderParam` is called, it simply appends the key-value pair to an internal `Header` instance, potentially overwriting any existing identically-named key/value pair. -**NOTE**: You do not need to set the `alg` or `zip` header parameters as JJWT will set them automatically -depending on the signature algorithm or compression algorithm used. +The downside with this approach is that you lose any type-safe setter methods or additional builder utility methods +available on the `Jwts.headerBuilder()` such as `setContentType`,`setKeyId`, `withX509Sha256Thumbprint`, etc. - -##### Header Instance +> **Note** +> +> **Automatic Headers**: You do not need to set the `alg`, `enc` or `zip` headers - JJWT will set them automatically +> as needed. -If you want to specify the entire header at once, you can use the `Jwts.header()` method and build up the header -paramters with it: + +#### Header Map -```java - -Header header = Jwts.header(); - -populate(header); //implement me - -String jws = Jwts.builder() - - .setHeader(header) - - // ... etc ... - -``` - -**NOTE**: Calling `setHeader` will overwrite any existing header name/value pairs with the same names that might have -already been set. In all cases however, JJWT will still set (and overwrite) any `alg` and `zip` headers regardless -if those are in the specified `header` object or not. - - -##### Header Map - -If you want to specify the entire header at once and you don't want to use `Jwts.header()`, you can use `JwtBuilder` -`setHeader(Map)` method instead: +If you want to specify the entire header at once, and you don't want to use `Jwts.headerBuilder()`, you can use +`JwtBuilder` `setHeader(Map)` method instead: ```java Map header = getMyHeaderMap(); //implement me -String jws = Jwts.builder() +String jwt = Jwts.builder() .setHeader(header) @@ -726,20 +865,76 @@ String jws = Jwts.builder() ``` +> **Warning** +> +> Per standard Java `setter` idioms, `setHeader` is a _full replacement_ operation - it will replace any +> and all existing header name/value pairs. -**NOTE**: Calling `setHeader` will overwrite any existing header name/value pairs with the same names that might have -already been set. In all cases however, JJWT will still set (and overwrite) any `alg` and `zip` headers regardless -if those are in the specified `header` object or not. +The downside with this approach is that you lose any type-safe setter methods or additional builder utility methods +available on the `Jwts.headerBuilder()` such as `setContentType`,`setKeyId`, `withX509Sha256Thumbprint`, etc. - -#### Claims +> **Note** +> +> **Automatic Headers**: You do not need to set the `alg`, `enc` or `zip` headers - JJWT will set them automatically +> as needed. -Claims are a JWT's 'body' and contain the information that the JWT creator wishes to present to the JWT recipient(s). + +### JWT Payload - +A JWT `payload` can be anything at all - anything that can be represented as a byte array, such as text, images, +documents, and more. But since a JWT `header` is always JSON, it makes sense that the `payload` could also be JSON, +especially for representing identity claims. + +As a result, the `JwtBuilder` supports two distinct payload options: + +* `setContent` if you would like the `payload` to be arbitrary byte array content, and +* `setClaims` (and supporting helper methods) if you would like the `payload` to be a JSON Claims `Object`. + +Either option may be used, but not both. Using both will cause `build()` to throw an exception. + + +#### Arbitrary Content + +You can set the JWT `payload` to be any arbitrary byte array content by using the `JwtBuilder` `setContent` method. +For example: + +```java +byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8); + +String jwt = Jwts.builder() + + .setContent(content, "text/plain") // <--- + + // ... etc ... + + .build(); +``` + +Notice this particular example of `setContent` uses the two-argument convenience variant: +1. The first argument is the actual byte content to set as the JWT payload +2. The second argument is a String identifier of an IANA Media Type. + +The second argument will cause the `JwtBuilder` to automatically set the `cty` (Content Type) header according to the +JWT specification's [recommended compact format](https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10). + +This two-argument variant is typically recommended over the single-argument `setContent(byte[])` method because it +guarantees the JWT recipient can inspect the `cty` header to determine how to convert the `payload` byte array into +a final form that the application can use. + +Without setting the `cty` header, the JWT recipient _must_ know via out-of-band (external) information how to process +the byte array, which is usually less convenient and always requires code changes if the content format ever changes. +For these reasons, it is strongly recommended to use the two-argument `setContent` method variant. + + +#### JWT Claims + +Instead of a content byte array, a JWT `payload` may contain assertions or claims for a JWT recipient. In +this case, the `payload` is a 'claims' JSON `Object`, and JJWT supports this with a type-safe `Claims` instance. + + ##### Standard Claims -The `JwtBuilder` provides convenient setter methods for standard registered Claim names defined in the JWT +The `JwtBuilder` provides convenient setter methods for standard registered Claim names defined in the JWT specification. They are: * `setIssuer`: sets the [`iss` (Issuer) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.1) @@ -767,11 +962,11 @@ String jws = Jwts.builder() /// ... etc ... ``` - + ##### Custom Claims If you need to set one or more custom claims that don't match the standard setter method claims shown above, you -can simply call `JwtBuilder` `claim` one or more times as needed: +can simply call the `JwtBuilder` `claim` method one or more times as needed: ```java String jws = Jwts.builder() @@ -782,14 +977,14 @@ String jws = Jwts.builder() ``` -Each time `claim` is called, it simply appends the key-value pair to an internal `Claims` instance, potentially +Each time `claim` is called, it simply appends the key-value pair to an internal `Claims` instance, potentially overwriting any existing identically-named key/value pair. -Obviously, you do not need to call `claim` for any [standard claim name](#jws-create-claims-standard) and it is -recommended instead to call the standard respective setter method as this enhances readability. +Obviously, you do not need to call `claim` for any [standard claim name](#jws-create-claims-standard), and it is +recommended instead to call the standard respective type-safe setter method as this enhances readability. - -###### Claims Instance + +##### Claims Instance If you want to specify all claims at once, you can use the `Jwts.claims()` method and build up the claims with it: @@ -808,13 +1003,16 @@ String jws = Jwts.builder() ``` -**NOTE**: Calling `setClaims` will overwrite any existing claim name/value pairs with the same names that might have -already been set. +> **Warning** +> +> Per standard Java `setter` idioms, calling `setClaims` will fully replace all existing claim name/value +pairs with the specified values. If you want to add (append) claims in bulk, and not fully replace them, use the +> `JwtBuilder`'s `addClaims` method instead. - -###### Claims Map + +##### Claims Map -If you want to specify all claims at once and you don't want to use `Jwts.claims()`, you can use `JwtBuilder` +If you want to specify all claims at once, and you don't want to use `Jwts.claims()`, you can use `JwtBuilder` `setClaims(Map)` method instead: ```java @@ -829,13 +1027,568 @@ String jws = Jwts.builder() ``` -**NOTE**: Calling `setClaims` will overwrite any existing claim name/value pairs with the same names that might have -already been set. +> **Warning** +> +> Per standard Java `setter` idioms, calling `setClaims` will fully replace all existing claim name/value +pairs with the specified values. If you want to add (append) claims in bulk, and not fully replace them, use the +> `JwtBuilder`'s `addClaims` method instead. + + +### JWT Compression + +If your JWT payload is large (contains a lot of data), you might want to compress the JWT to reduce its size. Note +that this is *not* a standard feature for all JWTs - only JWEs - and is not likely to be supported by other JWT +libraries for non-JWE tokens. JJWT supports compression for both JWSs and JWEs, however. + +Please see the main [Compression](#compression) section to see how to compress and decompress JWTs. + + +## Reading a JWT + +You read (parse) a JWT as follows: + +1. Use the `Jwts.parserBuilder()` method to create a `JwtParserBuilder` instance. +2. Optionally call `setKeyLocator`, `verifyWith` or `decryptWith` methods if you expect to parse [signed](#jws) or [encrypted](#jwe) JWTs. +3. Call the `build()` method on the `JwtParserBuilder` to create and return a thread-safe `JwtParser`. +4. Call one of the various `parse*` methods with your compact JWT string, depending on the type of JWT you expect. +5. Wrap the `parse*` call in a try/catch block in case parsing, signature verification, or decryption fails. + +For example: + +```java +Jwt jwt; + +try { + jwt = Jwts.parserBuilder() // (1) + + .setKeyLocator(keyLocator) // (2) dynamically locate signing or encryption keys + //.verifyWith(key) // or a static key used to verify all signed JWTs + //.decryptWith(key) // or a static key used to decrypt all encrypted JWTs + + .build() // (3) + + .parse(compact); // (4) or parseClaimsJws, parseClaimsJwe, parseContentJws, etc + + // we can safely trust the JWT + +catch (JwtException ex) { // (5) + + // we *cannot* use the JWT as intended by its creator +} +``` + +> **Note** +> +> **Type-safe JWTs:** If you are certain your parser will only ever encounter a specific kind of JWT (for example, you only +> ever use signed JWTs with `Claims` payloads, or encrypted JWTs with `byte[]` content payloads, etc), you can call the +> associated type-safe `parseClaimsJws`, `parseClaimsJwe`, (etc) method variant instead of the generic `parse` method. +> +> These `parse*` methods will return the type-safe JWT you are expecting, for example, a `Jws` or `Jwe` +> instead of a generic `Jwt` instance. + + +### Static Parsing Key + +If the JWT parsed is a JWS or JWE, a key will be necessary to verify the signature or decrypt it. If a JWS and +signature verification fails, or if a JWE and decryption fails, the JWT cannot be safely trusted and should be +discarded. + +So which key do we use? + +* If parsing a JWS and the JWS was signed with a `SecretKey`, the same `SecretKey` should be specified on the + `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .verifyWith(secretKey) // <---- + + .build() + .parseClaimsJws(jwsString); + ``` +* If parsing a JWS and the JWS was signed with a `PrivateKey`, that key's corresponding `PublicKey` (not the + `PrivateKey`) should be specified on the `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .verifyWith(publicKey) // <---- publicKey, not privateKey + + .build() + .parseClaimsJws(jwsString); + ``` +* If parsing a JWE and the JWE was encrypted with direct encryption using a `SecretKey`, the same `SecretKey` should be + specified on the `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .decryptWith(secretKey) // <---- + + .build() + .parseClaimsJwe(jweString); + ``` +* If parsing a JWE and the JWE was encrypted with a key algorithm using with a `PublicKey`, that key's corresponding + `PrivateKey` (not the `PublicKey`) should be specified on the `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .decryptWith(privateKey) // <---- privateKey, not publicKey + + .build() + .parseClaimsJwe(jweString); + ``` + +#### Multiple Keys? + +But you might have noticed something - what if your application doesn't use just a single `SecretKey` or `KeyPair`? What +if JWSs and JWEs can be created with different `SecretKey`s or public/private keys, or a combination of both? How do +you know which key to specify if you don't inspect the JWT first? + +In these cases, you can't call the `JwtParserBuilder`'s `verifyWith` or `decryptWith` methods with a single key - +instead, you'll need to configure a parsing Key Locator, discussed next. + + +### Dynamic Key Lookup + +It is common in many applications to receive JWTs that can be encrypted or signed by different cryptographic keys. For +example, maybe a JWT created to assert a specific user identity uses a Key specific to that exact user. Or perhaps JWTs +specific to a particular customer all use that customer's Key. Or maybe your application creates JWTs that are +encrypted with a key specific to your application for your own use (e.g. a user session token). + +In all of these and similar scenarios, you won't know which key was used to sign or encrypt a JWT until the JWT is +received, at parse time, so you can't 'hard code' any verification or decryption key using the `JwtParserBuilder`'s +`verifyWith` or `decryptWith` methods. Those are only to be used when the same key is used to verify or decrypt +*all* JWSs or JWEs, which won't work for dynamically signed or encrypted JWTs. + + +#### Key Locator + +If you need to support dynamic key lookup when encountering JWTs, you'll need to implement +the `Locator` interface and specify an instance on the `JwtParserBuilder` via the `setKeyLocator` method. For +example: + +```java +Locator keyLocator = getMyKeyLocator(); + +Jwts.parserBuilder() + + .setKeyLocator(keyLocator) // <---- + + .build() + // ... etc ... +``` + +A `Locator` is used to lookup _both_ JWS signature verification keys _and_ JWE decryption keys. You need to +determine which key to return based on information in the JWT `header`, for example: + +```java +public class MyKeyLocator extends LocatorAdapter { + + @Override + public Key locate(ProtectedHeader header) { // a JwsHeader or JweHeader + // implement me + } +} +``` + +The `JwtParser` will invoke the `locate` method after parsing the JWT `header`, but _before parsing the `payload`, +or verifying any JWS signature or decrypting any JWE ciphertext_. This allows you to inspect the `header` argument +for any information that can help you look up the `Key` to use for verifying _that specific jwt_. This is very +powerful for applications with more complex security models that might use different keys at different times or for +different users or customers. + + +#### Key Locator Strategy + +What data might you inspect to determine how to lookup a signature verification or decryption key? + +The JWT specifications' preferred approach is to set a `kid` (Key ID) header value when the JWT is being created, +for example: + +```java +Key key = getSigningKey(); // or getEncryptionKey() for JWE + +String keyId = getKeyId(key); //any mechanism you have to associate a key with an ID is fine + +String jws = Jwts.builder() + + .setHeader(Jwts.headerBuilder().setKeyId(keyId)) // <--- add `kid` header + + .signWith(key) // for JWS + //.encryptWith(key, keyAlg, encryptionAlg) // for JWE + .compact(); +``` + +Then during parsing, your `Locator` implementation can inspect the `header` to get the `kid` value and then use it +to look up the verification or decryption key from somewhere, like a database, keystore or Hardware Security Module +(HSM). For example: + +```java +public class MyKeyLocator extends LocatorAdapter { + + @Override + public Key locate(ProtectedHeader header) { // both JwsHeader and JweHeader extend ProtectedHeader + + //inspect the header, lookup and return the verification key + String keyId = header.getKeyId(); //or any other field that you need to inspect + + Key key = lookupKey(keyId); //implement me + + return key; + } +} +``` + +Note that inspecting the `header.getKeyId()` is just the most common approach to look up a key - you could +inspect any number of header fields to determine how to lookup the verification or decryption key. It is all based on +how the JWT was created. + +If you extend `LocatorAdapter` as shown above, but for some reason have different lookup strategies for +signature verification keys versus decryption keys, you can forego overriding the `locate(ProtectedHeader)` method +in favor of two respective `locate(JwsHeader)` and `locate(JweHeader)` methods: + +```java +public class MyKeyLocator extends LocatorAdapter { + + @Override + public Key locate(JwsHeader header) { + String keyId = header.getKeyId(); //or any other field that you need to inspect + return lookupSignatureVerificationKey(keyId); //implement me + } + + @Override + public Key locate(JweHeader header) { + String keyId = header.getKeyId(); //or any other field that you need to inspect + return lookupDecryptionKey(keyId); //implement me + } +} +``` +> **Note** +> +> **Simpler Lookup**: If possible, try to keep the key lookup strategy the same between JWSs and JWEs (i.e. using +> only `locate(ProtectedHeader)`), preferably using only +> the `kid` (Key ID) header value or perhaps a public key thumbprint. You will find the implementation is much +> simpler and easier to maintain over time, and also creates smaller headers for compact transmission. + + +#### Key Locator Return Values + +Regardless of which implementation strategy you choose, remember to return the appropriate type of key depending +on the type of JWS or JWE algorithm used. That is: + +* For JWS: + * For HMAC-based signature algorithms, the returned verification key should be a `SecretKey`, and, + * For asymmetric signature algorithms, the returned verification key should be a `PublicKey` (not a `PrivateKey`). +* For JWE: + * For JWE direct encryption, the returned decryption key should be a `SecretKey`. + * For password-based key derivation algorithms, the returned decryption key should be a + `io.jsonwebtoken.security.Password`. You can create a `Password` instance by calling + `Keys.forPassword(char[] passwordCharacters)`. + * For asymmetric key management algorithms, the returned decryption key should be a `PrivateKey` (not a `PublicKey`). + + +### Claim Assertions + +You can enforce that the JWT you are parsing conforms to expectations that you require and are important for your +application. + +For example, let's say that you require that the JWT you are parsing has a specific `sub` (subject) value, +otherwise you may not trust the token. You can do that by using one of the various `require`* methods on the +`JwtParserBuilder`: + +```java +try { + Jwts.parserBuilder().requireSubject("jsmith")/* etc... */.build().parse(s); +} catch (InvalidClaimException ice) { + // the sub field was missing or did not have a 'jsmith' value +} +``` + +If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, +you can catch either `MissingClaimException` or `IncorrectClaimException`: + +```java +try { + Jwts.parserBuilder().requireSubject("jsmith")/* etc... */.build().parse(s); +} catch(MissingClaimException mce) { + // the parsed JWT did not have the sub field +} catch(IncorrectClaimException ice) { + // the parsed JWT had a sub field, but its value was not equal to 'jsmith' +} +``` + +You can also require custom fields by using the `require(fieldName, requiredFieldValue)` method - for example: + +```java +try { + Jwts.parserBuilder().require("myfield", "myRequiredValue")/* etc... */.build().parse(s); +} catch(InvalidClaimException ice) { + // the 'myfield' field was missing or did not have a 'myRequiredValue' value +} +``` +(or, again, you could catch either `MissingClaimException` or `IncorrectClaimException` instead). + +Please see the `JwtParserBuilder` class and/or JavaDoc for a full list of the various `require`* methods you may use +for claims assertions. + + +### Accounting for Clock Skew + +When parsing a JWT, you might find that `exp` or `nbf` claim assertions fail (throw exceptions) because the clock on +the parsing machine is not perfectly in sync with the clock on the machine that created the JWT. This can cause +obvious problems since `exp` and `nbf` are time-based assertions, and clock times need to be reliably in sync for shared +assertions. + +You can account for these differences (usually no more than a few minutes) when parsing using the `JwtParserBuilder`'s +`setAllowedClockSkewSeconds`. For example: + +```java +long seconds = 3 * 60; //3 minutes + +Jwts.parserBuilder() + + .setAllowedClockSkewSeconds(seconds) // <---- + + // ... etc ... + .build() + .parse(jwt); +``` +This ensures that clock differences between the machines can be ignored. Two or three minutes should be more than +enough; it would be fairly strange if a production machine's clock was more than 5 minutes difference from most +atomic clocks around the world. + + +#### Custom Clock Support + +If the above `setAllowedClockSkewSeconds` isn't sufficient for your needs, the timestamps created +during parsing for timestamp comparisons can be obtained via a custom time source. Call the `JwtParserBuilder`'s +`setClock` method with an implementation of the `io.jsonwebtoken.Clock` interface. For example: + + ```java +Clock clock = new MyClock(); + +Jwts.parserBuilder().setClock(myClock) //... etc ... +``` + +The `JwtParser`'s default `Clock` implementation simply returns `new Date()` to reflect the time when parsing occurs, +as most would expect. However, supplying your own clock could be useful, especially when writing test cases to +guarantee deterministic behavior. + + +### JWT Decompression + +If you used JJWT to compress a JWT and you used a custom compression algorithm, you will need to tell the +`JwtParserBuilder` how to resolve your `CompressionCodec` to decompress the JWT. + +Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. + + +## Signed JWTs + +The JWT specification provides for the ability to +[cryptographically _sign_](https://en.wikipedia.org/wiki/Digital_signature) a JWT. Signing a JWT: + +1. guarantees the JWT was created by someone we know (it is authentic) as well as +2. guarantees that no-one has manipulated or changed the JWT after it was created (its integrity is maintained). + +These two properties - authenticity and integrity - assure us that a JWT contains information we can trust. If a +JWT fails authenticity or integrity checks, we should always reject that JWT because we can't trust it. + +But before we dig in to showing you how to create a JWS using JJWT, let's briefly discuss Signature Algorithms and +Keys, specifically as they relate to the JWT specifications. Understanding them is critical to being able to create a +JWS properly. + + +### Standard Signature Algorithms + +The JWT specifications identify 13 standard signature algorithms - 3 secret key algorithms and 10 asymmetric +key algorithms: + +| Identifier | Signature Algorithm | +| --- | --- | +| `HS256` | HMAC using SHA-256 | +| `HS384` | HMAC using SHA-384 | +| `HS512` | HMAC using SHA-512 | +| `ES256` | ECDSA using P-256 and SHA-256 | +| `ES384` | ECDSA using P-384 and SHA-384 | +| `ES512` | ECDSA using P-521 and SHA-512 | +| `RS256` | RSASSA-PKCS-v1_5 using SHA-256 | +| `RS384` | RSASSA-PKCS-v1_5 using SHA-384 | +| `RS512` | RSASSA-PKCS-v1_5 using SHA-512 | +| `PS256` | RSASSA-PSS using SHA-256 and MGF1 with SHA-2561 | +| `PS384` | RSASSA-PSS using SHA-384 and MGF1 with SHA-3841 | +| `PS512` | RSASSA-PSS using SHA-512 and MGF1 with SHA-5121 | +| `EdDSA` | Edwards-Curve Digital Signature Algorithm (EdDSA)2 | + +1. Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + +2. Requires Java 15 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + +These are all represented as constants in the `io.jsonwebtoken.Jwts.SIG` registry singleton. + + +### Signature Algorithms Keys + +What's really important about the above standard signature algorithms - other than their security properties - is that +the JWT specification [RFC 7518, Sections 3.2 through 3.5](https://tools.ietf.org/html/rfc7518#section-3) +_requires_ (mandates) that you MUST use keys that are sufficiently strong for a chosen algorithm. + +This means that JJWT - a specification-compliant library - will also enforce that you use sufficiently strong keys +for the algorithms you choose. If you provide a weak key for a given algorithm, JJWT will reject it and throw an +exception. + +This is not because we want to make your life difficult, we promise! The reason why the JWT specification, and +consequently JJWT, mandates key lengths is that the security model of a particular algorithm can completely break +down if you don't adhere to the mandatory key properties of the algorithm, effectively having no security at all. No +one wants completely insecure JWTs, right? Right! + +So what are the key strength requirements? + + +#### HMAC-SHA + +JWT HMAC-SHA signature algorithms `HS256`, `HS384`, and `HS512` require a secret key that is _at least_ as many bits as +the algorithm's signature (digest) length per [RFC 7512 Section 3.2](https://tools.ietf.org/html/rfc7518#section-3.2). +This means: + +* `HS256` is HMAC-SHA-256, and that produces digests that are 256 bits (32 bytes) long, so `HS256` _requires_ that you + use a secret key that is at least 32 bytes long. + +* `HS384` is HMAC-SHA-384, and that produces digests that are 384 bits (48 bytes) long, so `HS384` _requires_ that you + use a secret key that is at least 48 bytes long. + +* `HS512` is HMAC-SHA-512, and that produces digests that are 512 bits (64 bytes) long, so `HS512` _requires_ that you + use a secret key that is at least 64 bytes long. + + +#### RSA + +JWT RSA signature algorithms `RS256`, `RS384`, `RS512`, `PS256`, `PS384` and `PS512` all require a minimum key length +(aka an RSA modulus bit length) of `2048` bits per RFC 7512 Sections +[3.3](https://tools.ietf.org/html/rfc7518#section-3.3) and [3.5](https://tools.ietf.org/html/rfc7518#section-3.5). +Anything smaller than this (such as 1024 bits) will be rejected with an `WeakKeyException`. + +That said, in keeping with best practices and increasing key lengths for security longevity, JJWT +recommends that you use: + +* at least 2048 bit keys with `RS256` and `PS256` +* at least 3072 bit keys with `RS384` and `PS384` +* at least 4096 bit keys with `RS512` and `PS512` + +These are only JJWT suggestions and not requirements. JJWT only enforces JWT specification requirements and +for any RSA key, the requirement is the RSA key (modulus) length in bits MUST be >= 2048 bits. + + +#### Elliptic Curve + +JWT Elliptic Curve signature algorithms `ES256`, `ES384`, and `ES512` all require a key length +(aka an Elliptic Curve order bit length) equal to the algorithm signature's individual +`R` and `S` components per [RFC 7512 Section 3.4](https://tools.ietf.org/html/rfc7518#section-3.4). This means: + +* `ES256` requires that you use a private key that is exactly 256 bits (32 bytes) long. + +* `ES384` requires that you use a private key that is exactly 384 bits (48 bytes) long. + +* `ES512` requires that you use a private key that is exactly 521 bits (65 or 66 bytes) long (depending on format). + + +#### Edwards Curve + +The JWT Edwards Curve signature algorithm `EdDSA` supports two sizes of private and public `EdECKey`s (these types +were introduced in Java 15): + +* `Ed25519` algorithm keys must be 256 bits (32 bytes) long and produce signatures 512 bits (64 bytes) long. + +* `Ed448` algorithm keys must be 456 bits (57 bytes) long and produce signatures 912 bits (114 bytes) long. + + +#### Creating Safe Keys + +If you don't want to think about bit length requirements or just want to make your life easier, JJWT has +provided convenient builder classes that can generate sufficiently secure keys for any given +JWT signature algorithm you might want to use. + + +##### Secret Keys + +If you want to generate a sufficiently strong `SecretKey` for use with the JWT HMAC-SHA algorithms, use the respective +algorithm's `keyBuilder()` method: + +```java +SecretKey key = Jwts.SIG.HS256.keyBuilder().build(); //or HS384.keyBuilder() or HS512.keyBuilder() +``` + +Under the hood, JJWT uses the JCA default provider's `KeyGenerator` to create a secure-random key with the correct +minimum length for the given algorithm. + +If you want to specify a specific JCA `Provider` or `SecureRandom` to use during key generation, you may specify those +as builder arguments. For example: + +```java +SecretKey key = Jwts.SIG.HS256.keyBuilder().setProvider(aProvider).setRandom(aSecureRandom).build(); +``` + +If you need to save this new `SecretKey`, you can Base64 (or Base64URL) encode it: + +```java +String secretString = Encoders.BASE64.encode(key.getEncoded()); +``` + +Ensure you save the resulting `secretString` somewhere safe - +[Base64-encoding is not encryption](#base64-not-encryption), so it's still considered sensitive information. You can +further encrypt it, etc, before saving to disk (for example). + + +##### Asymmetric Keys + +If you want to generate sufficiently strong Elliptic Curve or RSA asymmetric key pairs for use with JWT ECDSA or RSA +algorithms, use an algorithm's respective `keyPairBuilder()` method: + +```java +KeyPair keyPair = Jwts.SIG.RS256.keyPairBuilder().build(); //or RS384, RS512, PS256, etc... +``` + +Once you've generated a `KeyPair`, you can use the private key (`keyPair.getPrivate()`) to create a JWS and the +public key (`keyPair.getPublic()`) to parse/verify a JWS. + +> **Note** +> +> **The `PS256`, `PS384`, and `PS512` algorithms require JDK 11 or a compatible JCA Provider +> (like BouncyCastle) in the runtime classpath.** +> **The `EdDSA`, `Ed25519` and `Ed448` algorithms require JDK 15 or a compatible JCA Provider +> (like BouncyCastle) in the runtime classpath.** +> If you want to use either set of algorithms, and you are on an earlier JDK that does not support them, +> see the [Installation](#Installation) section to see how to enable BouncyCastle. All other algorithms are +> natively supported by the JDK. + + +### Creating a JWS + +You create a JWS as follows: + +1. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. +2. Call `JwtBuilder` methods to set the `payload` content or claims and any header parameters as desired. +3. Specify the `SecretKey` or asymmetric `PrivateKey` you want to use to sign the JWT. +4. Finally, call the `compact()` method to compact and sign, producing the final jws. + +For example: + +```java +String jws = Jwts.builder() // (1) + + .setSubject("Bob") // (2) + + .signWith(key) // (3) <--- + + .compact(); // (4) +``` #### Signing Key -It is recommended that you specify the signing key by calling call the `JwtBuilder`'s `signWith` method and let JJWT +It is usually recommended to specify the signing key by calling the `JwtBuilder`'s `signWith` method and let JJWT determine the most secure algorithm allowed for the specified key.: ```java @@ -859,13 +1612,15 @@ algorithm and automatically set the `alg` header to `RS512`. The same selection logic applies for Elliptic Curve `PrivateKey`s. -**NOTE: You cannot sign JWTs with `PublicKey`s as this is always insecure.** JJWT will reject any specified -`PublicKey` for signing with an `InvalidKeyException`. +> **Note** +> +> **You cannot sign JWTs with `PublicKey`s as this is always insecure.** JJWT will reject any specified +> `PublicKey` for signing with an `InvalidKeyException`. ##### SecretKey Formats -If you want to sign a JWS using HMAC-SHA algorithms and you have a secret key `String` or +If you want to sign a JWS using HMAC-SHA algorithms, and you have a secret key `String` or [encoded byte array](https://docs.oracle.com/javase/8/docs/api/java/security/Key.html#getEncoded--), you will need to convert it into a `SecretKey` instance to use as the `signWith` method argument. @@ -896,7 +1651,7 @@ If your secret key is: ##### SignatureAlgorithm Override -In some specific cases, you might want to override JJWT's default selected algorithm for a given key. +In some specific cases, you might want to override JJWT's default selected signature algorithm for a given key. For example, if you have an RSA `PrivateKey` that is 2048 bits, JJWT would automatically choose the `RS256` algorithm. If you wanted to use `RS384` or `RS512` instead, you could manually specify it with the overloaded `signWith` method @@ -904,7 +1659,7 @@ that accepts the `SignatureAlgorithm` as an additional parameter: ```java - .signWith(privateKey, SignatureAlgorithm.RS512) // <--- + .signWith(privateKey, Jwts.SIG.RS512) // <--- .compact(); @@ -915,14 +1670,20 @@ prefers `RS512` for keys >= 4096 bits, followed by `RS384` for keys >= 3072 bits bits. **In all cases however, regardless of your chosen algorithms, JJWT will assert that the specified key is allowed to be -used for that algorithm according to the JWT specification requirements.** +used for that algorithm when possible according to the JWT specification requirements.** #### JWS Compression -If your JWT claims set is large (contains a lot of data), and you are certain that JJWT will also be the same library -that reads/parses your JWS, you might want to compress the JWS to reduce its size. Note that this is -*not* a standard feature for JWS and is not likely to be supported by other JWT libraries. +If your JWT payload is large (contains a lot of data), and you are certain that JJWT will also be the same library +that reads/parses your JWS, you might want to compress the JWS to reduce its size. + +> **Warning** +> +> **Not Standard for JWS**: JJWT supports compression for JWS, but it is not a standard feature for JWS. The +> JWT RFC specifications standardize this _only_ for JWEs, and it is not likely to be supported by other JWT libraries +> for JWS. Use JWS compression only if you are certain that JJWT (or another library that supports JWS compression) +> will be parsing the JWS Please see the main [Compression](#compression) section to see how to compress and decompress JWTs. @@ -931,16 +1692,13 @@ Please see the main [Compression](#compression) section to see how to compress a You read (parse) a JWS as follows: -1. Use the `Jwts.parserBuilder()` method to create a `JwtParserBuilder` instance. -2. Specify the `SecretKey` or asymmetric `PublicKey` you want to use to verify the JWS signature.1 +1. Use the `Jwts.parserBuilder()` method to create a `JwtParserBuilder` instance. +2. Call either [setKeyLocator](#key-locator) or `verifyWith` methods to determine the key used to verify the JWS signature. 3. Call the `build()` method on the `JwtParserBuilder` to return a thread-safe `JwtParser`. 4. Finally, call the `parseClaimsJws(String)` method with your jws `String`, producing the original JWS. 5. The entire call is wrapped in a try/catch block in case parsing or signature validation fails. We'll cover exceptions and causes for failure later. -1. If you don't know which key to use at the time of parsing, you can look up the key using a `SigningKeyResolver` -which [we'll cover later](#jws-read-key-resolver). - For example: ```java @@ -948,9 +1706,12 @@ Jws jws; try { jws = Jwts.parserBuilder() // (1) - .setSigningKey(key) // (2) + + .setKeyLocator(keyLocator) // (2) dynamically lookup verification keys based on each JWS + //.verifyWith(key) // or a static key used to verify all encountered JWSs + .build() // (3) - .parseClaimsJws(jwsString); // (4) + .parseClaimsJws(jwsString); // (4) or parseContentJws(jwsString) // we can safely trust the JWT @@ -960,24 +1721,28 @@ catch (JwtException ex) { // (5) } ``` -**NOTE: If you are expecting a JWS, always call `JwtParser`'s `parseClaimsJws` method** (and not one of the other similar methods -available) as this guarantees the correct security model for parsing signed JWTs. +> **Note** +> +> **Type-safe JWSs:** +> * If you are expecting a JWS with a Claims `payload`, call the `JwtParser`'s `parseClaimsJws` method. +> * If you are expecting a JWS with a content `payload`, call the `JwtParser`'s `parseContentJws` method. #### Verification Key -The most important thing to do when reading a JWS is to specify the key to use to verify the JWS's +The most important thing to do when reading a JWS is to specify the key used to verify the JWS's cryptographic signature. If signature verification fails, the JWT cannot be safely trusted and should be discarded. So which key do we use for verification? -* If the jws was signed with a `SecretKey`, the same `SecretKey` should be specified on the `JwtParserBuilder`. For example: +* If the jws was signed with a `SecretKey`, the same `SecretKey` should be specified on the `JwtParserBuilder`. +For example: ```java Jwts.parserBuilder() - .setSigningKey(secretKey) // <---- + .verifyWith(secretKey) // <---- .build() .parseClaimsJws(jwsString); @@ -988,212 +1753,794 @@ So which key do we use for verification? ```java Jwts.parserBuilder() - .setSigningKey(publicKey) // <---- publicKey, not privateKey + .verifyWith(publicKey) // <---- publicKey, not privateKey .build() .parseClaimsJws(jwsString); ``` + + +#### Verification Key Locator -But you might have noticed something - what if your application doesn't use just a single SecretKey or KeyPair? What +But you might have noticed something - what if your application doesn't use just a single `SecretKey` or `KeyPair`? What if JWSs can be created with different `SecretKey`s or public/private keys, or a combination of both? How do you know which key to specify if you can't inspect the JWT first? -In these cases, you can't call the `JwtParserBuilder`'s `setSigningKey` method with a single key - instead, you'll need -to use a `SigningKeyResolver`, covered next. - - -##### Signing Key Resolver - -If your application expects JWSs that can be signed with different keys, you won't call the `setSigningKey` method. -Instead, you'll need to implement the -`SigningKeyResolver` interface and specify an instance on the `JwtParserBuilder` via the `setSigningKeyResolver` method. -For example: - -```java -SigningKeyResolver signingKeyResolver = getMySigningKeyResolver(); - -Jwts.parserBuilder() - - .setSigningKeyResolver(signingKeyResolver) // <---- - - .build() - .parseClaimsJws(jwsString); -``` - -You can simplify things a little by extending from the `SigningKeyResolverAdapter` and implementing the -`resolveSigningKey(JwsHeader, Claims)` method. For example: - -```java -public class MySigningKeyResolver extends SigningKeyResolverAdapter { - - @Override - public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) { - // implement me - } -} -``` - -The `JwtParser` will invoke the `resolveSigningKey` method after parsing the JWS JSON, but _before verifying the -jws signature_. This allows you to inspect the `JwsHeader` and `Claims` arguments for any information that can -help you look up the `Key` to use for verifying _that specific jws_. This is very powerful for applications -with more complex security models that might use different keys at different times or for different users or customers. - -Which data might you inspect? - -The JWT specification's supported way to do this is to set a `kid` (Key ID) field in the JWS header when the JWS is -being created, for example: - -```java - -Key signingKey = getSigningKey(); - -String keyId = getKeyId(signingKey); //any mechanism you have to associate a key with an ID is fine - -String jws = Jwts.builder() - - .setHeaderParam(JwsHeader.KEY_ID, keyId) // 1 - - .signWith(signingKey) // 2 - - .compact(); -``` - -Then during parsing, your `SigningKeyResolver` can inspect the `JwsHeader` to get the `kid` and then use that value -to look up the key from somewhere, like a database. For example: - -```java -public class MySigningKeyResolver extends SigningKeyResolverAdapter { - - @Override - public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) { - - //inspect the header or claims, lookup and return the signing key - - String keyId = jwsHeader.getKeyId(); //or any other field that you need to inspect - - Key key = lookupVerificationKey(keyId); //implement me - - return key; - } -} -``` - -Note that inspecting the `jwsHeader.getKeyId()` is just the most common approach to look up a key - you could -inspect any number of header fields or claims to determine how to lookup the verification key. It is all based on -how the JWS was created. - -Finally remember that for HMAC algorithms, the returned verification key should be a `SecretKey`, and for asymmetric -algorithms, the key returned should be a `PublicKey` (not a `PrivateKey`). - - -#### Claim Assertions - -You can enforce that the JWS you are parsing conforms to expectations that you require and are important for your -application. - -For example, let's say that you require that the JWS you are parsing has a specific `sub` (subject) value, -otherwise you may not trust the token. You can do that by using one of the various `require`* methods on the -`JwtParserBuilder`: - -```java -try { - Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s); -} catch(InvalidClaimException ice) { - // the sub field was missing or did not have a 'jsmith' value -} -``` - -If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, -you can catch either `MissingClaimException` or `IncorrectClaimException`: - -```java -try { - Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s); -} catch(MissingClaimException mce) { - // the parsed JWT did not have the sub field -} catch(IncorrectClaimException ice) { - // the parsed JWT had a sub field, but its value was not equal to 'jsmith' -} -``` - -You can also require custom fields by using the `require(fieldName, requiredFieldValue)` method - for example: - -```java -try { - Jwts.parserBuilder().require("myfield", "myRequiredValue").setSigningKey(key).build().parseClaimsJws(s); -} catch(InvalidClaimException ice) { - // the 'myfield' field was missing or did not have a 'myRequiredValue' value -} -``` -(or, again, you could catch either `MissingClaimException` or `IncorrectClaimException` instead). - -Please see the `JwtParserBuilder` class and/or JavaDoc for a full list of the various `require`* methods you may use for claims -assertions. - - -#### Accounting for Clock Skew - -When parsing a JWT, you might find that `exp` or `nbf` claim assertions fail (throw exceptions) because the clock on -the parsing machine is not perfectly in sync with the clock on the machine that created the JWT. This can cause -obvious problems since `exp` and `nbf` are time-based assertions, and clock times need to be reliably in sync for shared -assertions. - -You can account for these differences (usually no more than a few minutes) when parsing using the `JwtParserBuilder`'s - `setAllowedClockSkewSeconds`. For example: - -```java -long seconds = 3 * 60; //3 minutes - -Jwts.parserBuilder() - - .setAllowedClockSkewSeconds(seconds) // <---- - - // ... etc ... - .build() - .parseClaimsJws(jwt); -``` -This ensures that clock differences between the machines can be ignored. Two or three minutes should be more than -enough; it would be fairly strange if a production machine's clock was more than 5 minutes difference from most -atomic clocks around the world. - - -##### Custom Clock Support - -If the above `setAllowedClockSkewSeconds` isn't sufficient for your needs, the timestamps created -during parsing for timestamp comparisons can be obtained via a custom time source. Call the `JwtParserBuilder`'s `setClock` - method with an implementation of the `io.jsonwebtoken.Clock` interface. For example: - - ```java -Clock clock = new MyClock(); - -Jwts.parserBuilder().setClock(myClock) //... etc ... -``` - -The `JwtParser`'s default `Clock` implementation simply returns `new Date()` to reflect the time when parsing occurs, -as most would expect. However, supplying your own clock could be useful, especially when writing test cases to -guarantee deterministic behavior. +In these cases, you can't call the `JwtParserBuilder`'s `verifyWith` method with a single key - instead, you'll need a +Key Locator. Please see the [Key Lookup](#key-locator) section to see how to dynamically obtain different keys when +parsing JWSs or JWEs. #### JWS Decompression -If you used JJWT to compress a JWS and you used a custom compression algorithm, you will need to tell the `JwtParserBuilder` -how to resolve your `CompressionCodec` to decompress the JWT. +If you used JJWT to compress a JWS and you used a custom compression algorithm, you will need to tell the +`JwtParserBuilder` how to resolve your `CompressionCodec` to decompress the JWT. Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. - + +## Encrypted JWTs + +The JWT specification also provides for the ability to encrypt and decrypt a JWT. Encrypting a JWT: + +1. guarantees that no-one other than the intended JWT recipient can see the JWT `payload` (it is confidential), and +2. guarantees that no-one has manipulated or changed the JWT after it was created (its integrity is maintained). + +These two properties - confidentiality and integrity - assure us that an encrypted JWT contains a `paylaod` that +no-one else can see, _nor_ has anyone changed or altered the data in transit. + +Encryption and confidentiality seem somewhat obvious: if you encrypt a message, it is confidential by the notion that +random 3rd parties cannot make sense of the encrypted message. But some might be surprised to know that **_general +encryption does _not_ guarantee that someone hasn't tampered/altered an encrypted message in transit_**. Most of us +assume that if a message can be decrypted, then the message would be authentic and unchanged - after all, if you can +decrypt it, it must not have been tampered with, right? Because if it was changed, decryption would surely fail, right? + +Unfortunately, this is not actually guaranteed in all cryptographic ciphers. There are certain attack vectors where +it is possible to change an encrypted payload (called 'ciphertext'), and the message recipient is still able to +successfully decrypt the (modified) payload. In these cases, the ciphertext integrity was not maintained - a +malicious 3rd party could intercept a message and change the payload content, even if they don't understand what is +inside the payload, and the message recipient could never know. + +To combat this, there is a category of encryption algorithms that ensures both confidentiality _and_ integrity of the +ciphertext data. These types of algorithms are called +[Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) algorithms. + +As a result, to ensure JWTs do not suffer from this problem, the JWE RFC specifications require that any encryption +algorithm used to encrypt a JWT _MUST_ be an Authenticated Encryption algorithm. JWT users can be sufficiently +confident their encrypted JWTs maintain the properties of both confidentiality and integrity. + + +### JWE Encryption Algorithms + +The JWT specification defines 6 standard Authenticated Encryption algorithms used to encrypt a JWT `payload`: + +| Identifier | Required Key Bit Length | Encryption Algorithm | +|--------------------------------- | ----------------------- | -------------------- | +| A128CBC‑HS256 | 256 | [AES_128_CBC_HMAC_SHA_256](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) authenticated encryption algorithm | +| `A192CBC-HS384` | 384 | [AES_192_CBC_HMAC_SHA_384](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4) authenticated encryption algorithm | +| `A256CBC-HS512` | 512 | [AES_256_CBC_HMAC_SHA_512](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5) authenticated encryption algorithm | +| `A128GCM` | 128 | AES GCM using 128-bit key1 | +| `A192GCM` | 192 | AES GCM using 192-bit key1 | +| `A256GCM` | 256 | AES GCM using 256-bit key1 | + +1. Requires Java 8+ or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + +These are all represented as constants in the `io.jsonwebtoken.Jwts.ENC` registry singleton as +implementations of the `io.jsonwebtoken.security.AeadAlgorithm` interface. + +As shown in the table above, each algorithm requires a key of sufficient length. The JWT specification +[RFC 7518, Sections 5.2.3 through 5.3](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) +_requires_ (mandates) that you MUST use keys that are sufficiently strong for a chosen algorithm. This means that +JJWT - a specification-compliant library - will also enforce that you use sufficiently strong keys +for the algorithms you choose. If you provide a weak key for a given algorithm, JJWT will reject it and throw an +exception. + +The reason why the JWT specification, and consequently JJWT, mandates key lengths is that the security model of a +particular algorithm can completely break down if you don't adhere to the mandatory key properties of the algorithm, +effectively having no security at all. + + +#### Symmetric Ciphers + +You might have noticed something about the above Authenticated Encryption algorithms: they're all variants of the +AES algorithm, and AES always uses a symmetric (secret) key to perform encryption and decryption. That's kind of +strange, isn't it? + +What about RSA and Elliptic Curve asymmetric key cryptography? And Diffie-Hellman key exchange? What about +password-based key derivation algorithms? Surely any of those could be desirable depending on the use case, no? + +Yes, they definitely can, and the JWT specifications do support them, albeit indirectly: those other +algorithms _are_ indeed supported and used, but they aren't used to encrypt the JWT `payload` directly. They are +used to _produce_ the actual key used to encrypt the `JWT` payload. + +This is all done via the JWT specification's concept of a Key Management Algorithm, covered next. After we cover that, +we'll show you how to encrypt and parse your own JWTs with the `JwtBuilder` and `JwtParserBuilder`. + + +### JWE Key Management Algorithms + +As stated above, all standard JWA Encryption Algorithms are AES-based authenticated encryption algorithms. So what +about RSA and Elliptic Curve cryptography? And password-based key derivation, or Diffie-Hellman exchange? + +All of those are supported as well, but they are not used directly for encryption. They are used to _produce_ the +key that will be used to directly encrypt the JWT `payload`. + +That is, JWT encryption can be thought of as a two-step process, shown in the following pseudocode: + +```groovy +Key algorithmKey = getKeyManagementAlgorithmKey(); // PublicKey, SecretKey, or Password + +SecretKey contentEncryptionKey = keyManagementAlgorithm.produceEncryptionKey(algorithmKey); // 1 + +byte[] ciphertext = encryptionAlgorithm.encrypt(payload, contentEncryptionKey); // 2 +``` + +Steps: + +1. Use the `algorithmKey` to produce the actual key that will be used to encrypt the payload. The JWT specifications + call this result the 'Content Encryption Key'. +2. Take the resulting Content Encryption Key and use it directly with the Authenticated Encryption algorithm to + actually encrypt the JWT `payload`. + +So why the indirection? Why not just use any `PublicKey`, `SecretKey` or `Password` to encrypt the `payload` +_directly_ ? + +There are quite a few reasons for this. + +1. Asymmetric key encryption (like RSA and Elliptic Curve) tends to be slow. Like _really_ slow. Symmetric key + cipher algorithms in contrast are _really fast_. This matters a lot in production applications that could be + handling a JWT on every HTTP request, which could be thousands per second. +2. RSA encryption (for example) can only encrypt a relatively small amount of data. A 2048-bit RSA key can only + encrypt up to a maximum of 245 bytes. A 4096-bit RSA key can only encrypt up to a maximum of 501 bytes. There are + plenty of JWTs that can exceed 245 bytes, and that would make RSA unusable. +3. Passwords usually make for very poor encryption keys - they often have poor entropy, or they themselves are + often too short to be used directly with algorithms that mandate minimum key lengths to help ensure safety. + +For these reasons and more, using one secure algorithm to generate or encrypt a key used for another (very fast) secure +algorithm has been proven to be a great way to increase security through many more secure algorithms while +also still resulting in very fast and secure output. This is after all how TLS (for https encryption) works - +two parties can use more complex cryptography (like RSA or Elliptic Curve) to negotiate a small, fast encryption key. +This fast encryption key is produced during the 'TLS handshake' and is called the TLS 'session key'. + +So the JWT specifications work much in the same way: one key from any number of various algorithm types can be used +to produce a final symmetric key, and that symmetric key is used to encrypt the JWT `payload`. + + +#### JWE Standard Key Management Algorithms + +The JWT specification defines 17 standard Key Management Algorithms used to produce the JWE +Content Encryption Key (CEK): + +| Identifier | Key Management Algorithm | +| --- |-------------------------------------------------------------------------------| +| `RSA1_5` | RSAES-PKCS1-v1_5 | +| `RSA-OAEP` | RSAES OAEP using default parameters | +| `RSA-OAEP-256` | RSAES OAEP using SHA-256 and MGF1 with SHA-256 | +| `A128KW` | AES Key Wrap with default initial value using 128-bit key | +| `A192KW` | AES Key Wrap with default initial value using 192-bit key | +| `A256KW` | AES Key Wrap with default initial value using 256-bit key | +| `dir` | Direct use of a shared symmetric key as the Content Encryption Key | +| `ECDH-ES` | Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF | +| `ECDH-ES+A128KW` | ECDH-ES using Concat KDF and CEK wrapped with "A128KW" | +| `ECDH-ES+A192KW` | ECDH-ES using Concat KDF and CEK wrapped with "A192KW" | +| `ECDH-ES+A256KW` | ECDH-ES using Concat KDF and CEK wrapped with "A256KW" | +| `A128GCMKW` | Key wrapping with AES GCM using 128-bit key3 | +| `A192GCMKW` | Key wrapping with AES GCM using 192-bit key3 | +| `A256GCMKW` | Key wrapping with AES GCM using 256-bit key3 | +| `PBES2-HS256+A128KW` | PBES2 with HMAC SHA-256 and "A128KW" wrapping3 | +| `PBES2-HS384+A192KW` | PBES2 with HMAC SHA-384 and "A192KW" wrapping3 | +| PBES2‑HS512+A256KW | PBES2 with HMAC SHA-512 and "A256KW" wrapping3 | + +3. Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. + +These are all represented as constants in the `io.jsonwebtoken.Jwts.KEY` registry singleton as +implementations of the `io.jsonwebtoken.security.KeyAlgorithm` interface. + +But 17 algorithms are a lot to choose from. When would you use them? The sections below describe when you might +choose each category of algorithms and how they behave. + + +##### RSA Key Encryption + +The JWT RSA key management algorithms `RSA1_5`, `RSA-OAEP`, and `RSA-OAEP-256` are used when you want to use the +JWE recipient's RSA _public_ key during encryption. This ensures that only the JWE recipient can decrypt +and read the JWE (using their RSA `private` key). + +During JWE creation, these algorithms: + +* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). +* Encrypt the JWE payload with the desired encryption algorithm using the new CEK, producing the JWE payload ciphertext. +* Encrypt the CEK itself with the specified RSA key wrap algorithm using the JWE recipient's RSA public key. +* Embed the payload ciphertext and encrypted CEK in the resulting JWE. + +During JWE decryption, these algorithms: + +* Retrieve the encrypted Content Encryption Key (CEK) embedded in the JWE. +* Decrypt the encrypted CEK with the discovered RSA key unwrap algorithm using the JWE recipient's RSA private key, + producing the decrypted Content Encryption Key (CEK). +* Decrypt the JWE ciphertext payload with the JWE's identified [encryption algorithm](#jwe-enc) using the decrypted CEK. + +> **Warning** +> +> RFC 7518 Sections [4.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.2) and +> [4.3](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.3) _require_ (mandate) that RSA keys >= 2048 bits +> MUST be used with these algorithms. JJWT will throw an exception if it detects weaker keys being used. + + +##### AES Key Encryption + +The JWT AES key management algorithms `A128KW`, `A192KW`, `A256KW`, `A128GCMKW`, `A192GCMKW`, and `A256GCMKW` are +used when you have a symmetric secret key, but you don't want to use that secret key to directly +encrypt/decrypt the JWT. + +Instead, a new secure-random key is generated each time a JWE is created, and that new/random key is used to directly +encrypt/decrypt the JWT payload. The secure-random key is itself encrypted with your symmetric secret key +using the AES Wrap algorithm, and the encrypted key is embedded in the resulting JWE. + +This allows the JWE to be encrypted with a random short-lived key, reducing material exposure of the potentially +longer-lived symmetric secret key. + +Because these particular algorithms use a symmetric secret key, they are best suited when the JWE creator and +receiver are the same, ensuring the secret key does not need to be shared with multiple parties. + +During JWE creation, these algorithms: + +* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). +* Encrypt the JWE payload with the desired encryption algorithm using the new CEK, producing the JWE payload ciphertext. +* Encrypt the CEK itself with the specified AES key algorithm (either AES Key Wrap or AES with GCM encryption), + producing the encrypted CEK. +* Embed the payload ciphertext and encrypted CEK in the resulting JWE. + +During JWE decryption, these algorithms: + +* Retrieve the encrypted Content Encryption Key (CEK) embedded in the JWE. +* Decrypt the encrypted CEK with the discovered AES key algorithm using the symmetric secret key. +* Decrypt the JWE ciphertext payload with the JWE's identified [encryption algorithm](#jwe-enc) using the decrypted CEK. + +> **Warning** +> +> The symmetric key used for the AES key algorithms MUST be 128, 192 or 256 bits as required by the specific AES +> key algorithm. JJWT will throw an exception if it detects weaker keys than what is required. + + +##### Direct Key Encryption + +The JWT `dir` (direct) key management algorithm is used when you have a symmetric secret key, and you want to use it +to directly encrypt the JWT payload. + +Because this algorithm uses a symmetric secret key, it is best suited when the JWE creator and receiver are the +same, ensuring the secret key does not need to be shared with multiple parties. + +This is the simplest key algorithm for direct encryption that does not perform any key encryption. It is essentially +a 'no op' key algorithm, allowing the shared key to be used to directly encrypt the JWT payload. + +During JWE creation, this algorithm: + +* Encrypts the JWE payload with the desired encryption algorithm directly using the symmetric secret key, + producing the JWE payload ciphertext. +* Embeds the payload ciphertext in the resulting JWE. + +Note that because this algorithm does not produce an encrypted key value, an encrypted CEK is _not_ embedded in the +resulting JWE. + +During JWE decryption, this algorithm decrypts the JWE ciphertext payload with the JWE's +identified [encryption algorithm](#jwe-enc) directly using the symmetric secret key. No encrypted CEK is used. + +> **Warning** +> +> The symmetric secret key MUST be 128, 192 or 256 bits as required by the associated +> [AEAD encryption algorithm](#jwe-enc) used to encrypt the payload. JJWT will throw an exception if it detects +> weaker keys than what is required. + + +##### Password-Based Key Encryption + +The JWT password-based key encryption algorithms `PBES2-HS256+A128KW`, `PBES2-HS384+A192KW`, and `PBES2-HS512+A256KW` +are used when you want to use a password (character array) to encrypt and decrypt a JWT. + +However, because passwords are usually too weak or problematic to use directly in cryptographic contexts, these +algorithms utilize key derivation techniques with work factors (e.g. computation iterations) and secure-random salts +to produce stronger cryptographic keys suitable for cryptographic operations. + +This allows the payload to be encrypted with a random short-lived cryptographically-stronger key, reducing the need to +expose the longer-lived (and potentially weaker) password. + +Because these algorithms use a secret password, they are best suited when the JWE creator and receiver are the +same, ensuring the secret password does not need to be shared with multiple parties. + +During JWE creation, these algorithms: + +* Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). +* Encrypt the JWE payload with the desired encryption algorithm using the new CEK, producing the JWE payload ciphertext. +* Derive a 'key encryption key' (KEK) with the desired "PBES2 with HMAC SHA" algorithm using the password, a suitable + number of computational iterations, and a secure-random salt value. +* Encrypt the generated CEK with the corresponding AES Key Wrap algorithm using the password-derived KEK. +* Embed the payload ciphertext and encrypted CEK in the resulting JWE. + +> **Note** +> +> **Secure defaults**: When using these algorithms, if you do not specify a work factor (i.e. number of computational +> iterations), JJWT will automatically use an +> [OWASP PBKDF2 recommended](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) +> default appropriate for the specified `PBES2` algorithm. + +During JWE decryption, these algorithms: + +* Retrieve the encrypted Content Encryption Key (CEK) embedded in the JWE. +* Derive the 'key encryption key' (KEK) with the discovered "PBES2 with HMAC SHA" algorithm using the password and the + number of computational iterations and secure-random salt value discovered in the JWE header. +* Decrypt the encrypted CEK with the corresponding AES Key Unwrap algorithm using the password-derived KEK. +* Decrypt the JWE ciphertext payload with the JWE's identified [encryption algorithm](#jwe-enc) using the decrypted CEK. + + +##### Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement + +The JWT Elliptic Curve Diffie-Hellman Ephemeral Static key agreement algorithms `ECDH-ES`, `ECDH-ES+A128KW`, +`ECDH-ES+A192KW`, and `ECDH-ES+A256KW` are used when you want to use the JWE recipient's Elliptic Curve _public_ key +during encryption. This ensures that only the JWE recipient can decrypt and read the JWE (using their Elliptic Curve +_private_ key). + +During JWE creation, these algorithms: + +* Obtain the Content Encryption Key (CEK) used to encrypt the JWE payload as follows: + * Inspect the JWE recipient's Elliptic Curve public key and determine its Curve. + * Generate a new secure-random ephemeral Ellipic Curve public/private key pair on this same Curve. + * Add the ephemeral EC public key to the JWE + [epk header](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1) for inclusion in the final JWE. + * Produce an ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC public key + and the ephemeral EC private key. + * Derive a symmetric secret key with the Concat Key Derivation Function + ([NIST.800-56A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf), Section 5.8.1) using + this ECDH shared secret and any provided + [PartyUInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.2) and/or + [PartyVInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.3). + * If the key algorithm is `ECDH-ES`: + * Use the Concat KDF-derived symmetric secret key directly as the Content Encryption Key (CEK). No encrypted key + is created, nor embedded in the resulting JWE. + * Otherwise, if the key algorithm is `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, or `ECDH-ES+A256KW`: + * Generate a new secure-random Content Encryption Key (CEK) suitable for the desired [encryption algorithm](#jwe-enc). + * Encrypt this new CEK with the corresponding AES Key Wrap algorithm using the Concat KDF-derived secret key, + producing the encrypted CEK. + * Embed the encrypted CEK in the resulting JWE. +* Encrypt the JWE payload with the desired encryption algorithm using the obtained CEK, producing the JWE payload + ciphertext. +* Embed the payload ciphertext in the resulting JWE. + +During JWE decryption, these algorithms: + +* Obtain the Content Encryption Key (CEK) used to decrypt the JWE payload as follows: + * Retrieve the required ephemeral Elliptic Curve public key from the JWE's + [epk header](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.1). + * Ensure the ephemeral EC public key exists on the same curve as the JWE recipient's EC private key. + * Produce the ECDH shared secret with the ECDH Key Agreement algorithm using the JWE recipient's EC private key + and the ephemeral EC public key. + * Derive a symmetric secret key with the Concat Key Derivation Function + ([NIST.800-56A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf), Section 5.8.1) using + this ECDH shared secret and any + [PartyUInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.2) and/or + [PartyVInfo](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.1.3) found in the JWE header. + * If the key algorithm is `ECDH-ES`: + * Use the Concat KDF-derived secret key directly as the Content Encryption Key (CEK). No encrypted key is used. + * Otherwise, if the key algorithm is `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, or `ECDH-ES+A256KW`: + * Obtain the encrypted key ciphertext embedded in the JWE. + * Decrypt the encrypted key ciphertext with the associated AES Key Unwrap algorithm using the Concat KDF-derived + secret key, producing the unencrypted Content Encryption Key (CEK). +* Decrypt the JWE payload ciphertext with the JWE's discovered encryption algorithm using the obtained CEK. + + +### Creating a JWE + +Now that we know the difference between a JWE Encryption Algorithm and a JWE Key Management Algorithm, how do we use +them to encrypt a JWT? + +You create an encrypted JWT (called a 'JWE') as follows: + +1. Use the `Jwts.builder()` method to create a `JwtBuilder` instance. +2. Call `JwtBuilder` methods to set the `payload` content or claims and any [header](#jws-create-header) parameters as desired. +3. Call the `encryptWith` method, specifying the Key, Key Algorithm, and Encryption Algorithm you want to use. +4. Finally, call the `compact()` method to compact and encrypt, producing the final jwe. + +For example: + +```java +String jwe = Jwts.builder() // (1) + + .setSubject("Bob") // (2) + + .encryptWith(key, keyAlgorithm, encryptionAlgorithm) // (3) + + .compact(); // (4) +``` + +Before calling `compact()`, you may set any [header](#jws-create-header) parameters and [claims](#jws-create-claims) +exactly the same way as described for JWS. + + +#### JWE Compression + +If your JWT payload or Claims set is large (contains a lot of data), you might want to compress the JWE to reduce +its size. Please see the main [Compression](#compression) section to see how to compress and decompress JWTs. + + +### Reading a JWE + +You read (parse) a JWE as follows: + +1. Use the `Jwts.parserBuilder()` method to create a `JwtParserBuilder` instance. +2. Call either [setKeyLocator](#key-locator) or `decryptWith` methods to determine the key used to decrypt the JWE. +4. Call the `JwtParserBuilder`'s `build()` method to create a thread-safe `JwtParser`. +5. Parse the jwe string with the `JwtParser`'s `parseClaimsJwe` or `parseContentJwe` method. +6. Wrap the entire call is in a try/catch block in case decryption or integrity verification fails. + +For example: + +```java +Jwe jwe; + +try { + jwe = Jwts.parserBuilder() // (1) + + .setKeyLocator(keyLocator) // (2) dynamically lookup decryption keys based on each JWE + //.decryptWith(key) // or a static key used to decrypt all encountered JWEs + + .build() // (3) + .parseClaimsJwe(jweString); // (4) or parseContentJwe(jweString); + + // we can safely trust the JWT + +catch (JwtException ex) { // (5) + + // we *cannot* use the JWT as intended by its creator +} +``` + +> **Note** +> +> **Type-safe JWEs:** +> * If you are expecting a JWE with a Claims `payload`, call the `JwtParser`'s `parseClaimsJwe` method. +> * If you are expecting a JWE with a content `payload`, call the `JwtParser`'s `parseContentJwe` method. + + +#### Decryption Key + +The most important thing to do when reading a JWE is to specify the key used during decryption. If decryption or +integrity protection checks fail, the JWT cannot be safely trusted and should be discarded. + +So which key do we use for decryption? + +* If the jwe was encrypted _directly_ with a `SecretKey`, the same `SecretKey` must be specified on the + `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .decryptWith(secretKey) // <---- + + .build() + .parseClaimsJws(jwsString); + ``` +* If the jwe was encrypted using a key produced by a Password-based key derivation `KeyAlgorithm`, the same + `Password` must be specified on the `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .decryptWith(password) // <---- an `io.jsonwebtoken.security.Password` instance + + .build() + .parseClaimsJws(jwsString); + ``` +* If the jwe was encrypted with a key produced by an asymmetric `KeyAlgorithm`, the corresponding `PrivateKey` (not + the `PublicKey`) must be specified on the `JwtParserBuilder`. For example: + + ```java + Jwts.parserBuilder() + + .decryptWith(privateKey) // <---- a `PrivateKey`, not a `PublicKey` + + .build() + .parseClaimsJws(jwsString); + ``` + + +#### Decryption Key Locator + +What if your application doesn't use just a single `SecretKey` or `KeyPair`? What +if JWEs can be created with different `SecretKey`s, `Password`s or public/private keys, or a combination of all of +them? How do you know which key to specify if you can't inspect the JWT first? + +In these cases, you can't call the `JwtParserBuilder`'s `decryptWith` method with a single key - instead, you'll need +to use a Key `Locator`. Please see the [Key Lookup](#key-locator) section to see how to dynamically obtain different +keys when parsing JWSs or JWEs. + + +#### JWE Decompression + +If a JWE is compressed using the `DEF` ([DEFLATE](https://www.rfc-editor.org/rfc/rfc1951)) or `GZIP` +([GZIP](https://www.rfc-editor.org/rfc/rfc1952.html)) compression algorithms, it will automatically be decompressed +after decryption, and there is nothing you need to configure. + +If, however, a custom compression algorithm was used to compress the JWE, you will need to tell the +`JwtParserBuilder` how to resolve your `CompressionCodec` to decompress the JWT. + +Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing. + + +## JSON Web Keys (JWKs) + +[JSON Web Keys](https://www.rfc-editor.org/rfc/rfc7517.html) (JWKs) are JSON serializations of cryptographic keys, +allowing key material to be embedded in JWTs or transmitted between parties in a standard JSON-based text format. They +are essentially a JSON-based alternative to other text-based key formats, such as the +[DER, PEM and PKCS12](https://serverfault.com/a/9717) text strings or files commonly used when configuring TLS on web +servers, for example. + +For example, an identity web service may expose its RSA or Elliptic Curve Public Keys to 3rd parties in the JWK format. +A client may then parse the public key JWKs to verify the service's [JWS](#jws) tokens, as well as send encrypted +information to the service using [JWE](#jwe)s. + +JWKs can be converted to and from standard Java `Key` types as expected using the same builder/parser patterns we've +seen for JWTs. + + +### Create a JWK + +You create a JWK as follows: + +1. Use the `Jwks.builder()` method to create a `JwkBuilder` instance. +2. Call the `forKey` method with the Java key you wish to represent as a JWK. +3. Call builder methods to set any additional key fields or metadata, such as a `kid` (Key ID), X509 Certificates, + etc as desired. +4. Call the `build()` method to produce the resulting JWK. + +For example: + +```java +SecretKey key = getSecretKey(); // or RSA or EC PublicKey or PrivateKey +SecretJwk = Jwts.builder().forKey(key) // (1) and (2) + + .setId("mySecretKeyId") // (3) + // ... etc ... + + .build(); // (4) +``` + + +### Read a JWK + +You can read/parse a JWK by building a `JwkParser` and parsing the JWK JSON string with its `parse` method: + +```java +String json = getJwkJsonString(); +Jwk jwk = Jwks.parser() + //.setProvider(aJcaProvider) // optional + //.deserializeJsonWith(deserializer) // optional + .build() // create the parser + .parse(json); // actually parse the JSON + +Key key = jwk.toKey(); // convert to a Java Key instance +``` +As shown above you can specify a custom JCA Provider or [JSON deserializer](#json) in the same way as the `JwtBuilder`. + + +### PrivateKey JWKs + +Unlike Java, the JWA specification requires a private JWKs to contain _both_ public key _and_ private key material +(see [RFC 7518, Section 6.1.1](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.2) and +[RFC 7518, Section 6.3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2)). + +In this sense, a private JWK (represented as a `PrivateJwk` or a subtype, such as `RsaPrivateJwk`, `EcPrivateJwk`, etc) +can be thought of more like a Java `KeyPair` instance. Consequently, when creating a `PrivateJwk` instance, +the `PrivateKey`'s corresponding `PublicKey` is required. + + +#### Private JWK `PublicKey` + +If you do not provide a `PublicKey` when creating a `PrivateJwk`, JJWT will automatically derive the `PublicKey` from +the `PrivateKey` instance if possible. However, because this can add +some computing time, it is typically recommended to provide the `PublicKey` when possible to avoid this extra work. + +For example: + +```java +RSAPrivateKey rsaPrivateKey = getRSAPrivateKey(); // or ECPrivateKey + +RsaPrivateJwk jwk = Jwks.builder().forKey(rsaPrivateKey) + + //.setPublicKey(rsaPublicKey) // optional, but recommended to avoid extra computation work + + .build(); +``` + + +#### Private JWK from KeyPair + +If you have a Java `KeyPair` instance, then you have both the public and private key material necessary to create a +`PrivateJwk`. For example: + +```java +KeyPair rsaKeyPair = getRSAKeyPair(); +RsaPrivateJwk rsaPrivJwk = Jwks.builder().forRsaKeyPair(rsaKeyPair).build(); + +KeyPair ecKeyPair = getECKeyPair(); +EcPrivateJwk ecPrivJwk = Jwks.builder().forEcKeyPair(ecKeyPair).build(); + +KeyPair edEcKeyPair = getEdECKeyPair(); +OctetPrivateJwk edEcPrivJwk = Jwks.builder().forOctetKeyPair(edEcKeyPair).build(); +``` + +Note that: +* An exception will thrown when calling `forRsaKeyPair` if the specified `KeyPair` instance does not contain +`RSAPublicKey` and `RSAPrivateKey` instances. +* Similarly, an exception will be thrown when calling `forEcKeyPair` if +the `KeyPair` instance does not contain `ECPublicKey` and `ECPrivateKey` instances. +* Finally, an exception will be +thrown when calling `forOctetKeyPair` if the `KeyPair` instance does not contain X25519, X448, Ed25519, or Ed448 keys +(introduced in JDK 11 and 15 or when using BouncyCastle). + + +#### Private JWK Public Conversion + +Because private JWKs contain public key material, you can always obtain the private JWK's corresponding public JWK and +Java `PublicKey` or `KeyPair`. For example: + +```java +RsaPrivateJwk privateJwk = Jwks.builder().forKey(rsaPrivateKey).build(); // or ecPrivateKey or edEcPrivateKey + +// Get the matching public JWK and/or PublicKey: +RsaPublicJwk pubJwk = privateJwk.toPublicJwk(); // JWK instance +RSAPublicKey pubKey = pubJwk.toKey(); // Java PublicKey instance +KeyPair pair = privateJwk.toKeyPair(); // io.jsonwebtoken.security.KeyPair retains key types +java.security.KeyPair jdkPair = pair.toJavaKeyPair(); // does not retain pub/private key types +``` + + +### JWK Thumbprints + +A [JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html) is a digest (aka hash) of a canonical JSON +representation of a JWK's public properties. 'Canonical' in this case means that only RFC-specified values in any JWK +are used in an exact order thumbprint calculation. This ensures that anyone can calculate a JWK's same exact +thumbprint, regardless of custom fields or JSON key/value ordering differences in a JWK. + +All `Jwk` instances support [JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html)s via the +`thumbprint()` and `thumbprint(HashAlgorithm)` methods: + +```java +HashAlgorithm hashAlg = Jwks.HASH.SHA256; // or SHA384, SHA512, etc. + +Jwk jwk = Jwks.builder(). /* ... */ .build(); + +JwkThumbprint sha256Thumbprint = jwk.thumbprint(); // SHA-256 thumbprint by default + +JwkThumbprint anotherThumbprint = jwk.thumbprint(hashAlg); // thumbprint using specified hash algorithm +``` + +The resulting `JwkThumbprint` instance provides some useful methods: + +* `jwkThumbprint.toByteArray()`: the thumbprint's actual digest bytes - i.e. the raw output from the hash algorithm +* `jwkThumbprint.toString()`: the digest bytes as a Base64URL-encoded string +* `jwkThumbprint.getHashAlgorithm()`: the specific `HashAlgorithm` used to compute the thumbprint +* `jwkThumbprint.toURI()`: the thumbprint's canonical URI as defined by the [JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) specification + + +#### JWK Thumbprint as a Key ID + +Because a thumbprint is an order-guaranteed unique digest of a JWK, JWK thumbprints are often used as convenient +unique identifiers for a JWK (e.g. the JWK's `kid` (Key ID) value). These identifiers can be useful when +[locating keys](#key-locator) for JWS signature verification or JWE decryption, for example. + +For example: + +```java +String kid = jwk.thumbprint().toString(); // Thumbprint bytes as a Base64URL-encoded string +Key key = findKey(kid); +assert jwk.toKey().equals(key); +``` + +However, because `Jwk` instances are immutable, you can't set the key id after the JWK is created. For example, the +following is not possible: + +```java +String kid = jwk.thumbprint().toString(); +jwk.setId(kid) // Jwks are immutable - there is no `setId` method +``` + +Instead, you may use the `setIdFromThumbprint` methods on the `JwkBuilder` when creating a `Jwk`: + +```java +Jwk jwk = Jwks.builder().forKey(aKey) + + .setIdFromThumbprint() // or setIdFromThumbprint(hashAlgorithm) + + .build(); +``` + +Calling either `setIdFromThumbprint` method will ensure that calling `jwk.getId()` equals `thumbprint.toString()` +(which is `Encoders.BASE64URL.encode(thumbprint.toByteArray())`). + + +#### JWK Thumbprint URI + +A JWK's thumbprint's canonical URI as defined by the [JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) +specification may be obtained by calling the thumbprint's `toURI()` method: + +```java +URI canonicalThumbprintURI = jwk.thumbprint().toURI(); +``` + +Per the RFC specification, if you call `canonicalThumbprintURI.toString()`, you would see a string that looks like this: + +```text +urn:ietf:params:oauth:jwk-thumbprint:HASH_ALG_ID:BASE64URL_DIGEST +``` + +where: +* `urn:ietf:params:oauth:jwk-thumbprint:` is the URI scheme+prefix +* `HASH_ALG_ID` is the standard identifier used to compute the thumbprint as defined in the + [IANA Named Information Hash Algorithm Registry](https://www.iana.org/assignments/named-information/named-information.xhtml). + This is the same as `thumbprint.getHashAlgorithm().getId()`. +* `BASE64URL_DIGEST` is the Base64URL-encoded thumbprint bytes, equal to `jwkThumbprint.toString()`. + + +### JWK Security Considerations + +Because they contain secret or private key material, `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, +`EcPrivateJwk`, etc) instances should be used with great care and never accidentally transmitted to 3rd parties. + +Even so, JJWT's `Jwk` implementations will suppress certain values in `toString()` output for safety as described +next. + + +#### JWK `toString()` Safety + +Because it would be incredibly easy to accidentally print key material to `System.out.println()` or application +logs, all `Jwk` implementations will print redacted values instead of actual secret or private key material. + +For example, consider the following Secret JWK JSON example from +[RFC 7515, Appendix A.1.1](https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1): + +```json +{ + "kty": "oct", + "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", + "kid": "HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example." +} +``` + +The `k` value (`AyAyM1SysPpby...`) reflects secure key material and should never be accidentially +exposed. + +If you were to parse this JSON as a `Jwk`, calling `toString()` will _NOT_ print this value. It will +instead print the string literal `` for any secret or private key data field. For example: + +```java +String json = getExampleSecretKeyJson(); +Jwk jwk = Jwks.parser().build().parse(json); + +System.out.printn(jwk); +``` + +This code would print the following string literal to the System console: + +```text +{kty=oct, k=, kid=HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example.} +``` + +This is true for all secret or private key members in `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`, +`EcPrivateJwk`, etc) instances. ## Compression -**The JWT specification only standardizes this feature for JWEs (Encrypted JWTs) and not JWSs (Signed JWTs), however -JJWT supports both**. If you are positive that a JWS you create with JJWT will _also_ be parsed with JJWT, you -can use this feature with JWSs, otherwise it is best to only use it for JWEs. +**The JWT specification only standardizes this feature for JWEs (Encrypted JWTs) however JJWT supports it for JWS +(Signed JWTs) as well**. If you are positive that a JWT you create with JJWT +will _also_ be parsed with JJWT, you can use this feature with both JWEs and JWSs, otherwise it is best to only use it +for JWEs. -If a JWT's Claims set is sufficiently large - that is, it contains a lot of name/value pairs, or individual values are -very large or verbose - you can reduce the size of the created JWS by compressing the claims body. +If a JWT's `payload` is sufficiently large - that is, it is a large content byte array or JSON with a lot of +name/value pairs (or individual values are very large or verbose) - you can reduce the size of the compact JWT by +compressing the payload. -This might be important to you if the resulting JWS is used in a URL for example, since URLs are best kept under +This might be important to you if the resulting JWT is used in a URL for example, since URLs are best kept under 4096 characters due to browser, user mail agent, or HTTP gateway compatibility issues. Smaller JWTs also help reduce bandwidth utilization, which may or may not be important depending on your application's volume or needs. @@ -1209,42 +2556,89 @@ example: ``` If you use the `DEFLATE` or `GZIP` Compression Codecs - that's it, you're done. You don't have to do anything during -parsing or configure the `JwtParser` for compression - JJWT will automatically decompress the body as expected. +parsing or configure the `JwtParserBuilder` for compression - JJWT will automatically decompress the payload as +expected. + +> **Note** +> +> JJWT does not support compression for Unprotected JWTs because they are susceptible to various compression +> vulnerability attacks (memory exhaustion, denial of service, etc.). ### Custom Compression Codec -If however, you used your own custom compression codec when creating the JWT (via `JwtBuilder` `compressWith`), then -you need to supply the codec to the `JwtParserBuilder` using the `setCompressionCodecResolver` method. For example: +If the default `DEFLATE` or `GZIP` compression codecs are not suitable for your needs, you can create your own +`CompressionCodec` implementation(s). + +Just as you would with the default codecs, you may specify that you want a JWT compressed by calling the `JwtBuilder`'s +`compressWith` method, supplying your custom implementation instance. When you call `compressWith`, the JWT `payload` +will be compressed with your algorithm, and the +[`zip` (Compression Algorithm)](https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.3) +header will automatically be set to the value returned by your codec's `getId()` method as specified in the JWT +specification. + +However, the `JwtParser` needs to be aware of this custom codec as well, so it can use it while parsing. You do this +by calling the `JwtParserBuilder`'s `addCompressionCodecs` method. For example: ```java -CompressionCodecResolver ccr = new MyCompressionCodecResolver(); +CompressionCodec myCodec = new MyCompressionCodec(); Jwts.parserBuilder() - .setCompressionCodecResolver(ccr) // <---- + .addCompressionCodecs(Collections.of(myCodec)) // <---- // .. etc ... ``` -Typically a `CompressionCodecResolver` implementation will inspect the `zip` header to find out what algorithm was +This adds additional `CompressionCodec` implementations to the parser's overall total set of supported codecs (which +already includes the `DEFLATE` and `GZIP` codecs by default). + +The parser will then automatically check to see if the JWT `zip` header has been set to see if a compression codec +algorithm has been used to compress the JWT. If set, the parser will automatically look up your `CompressionCodec` by +its `getId()` value, and use it to decompress the JWT. + + +### Compression Codec Locator + +If for some reason the default `addCompressionCodecs` method and lookup-by-id behavior already supported by the +`JwtParserBuilder` is not sufficient for your needs, you can implement your own `Locator` to look +up the codec. + +Typically, a `Locator` implementation will inspect the `zip` header to find out what algorithm was used and then return a codec instance that supports that algorithm. For example: ```java -public class MyCompressionCodecResolver implements CompressionCodecResolver { +public class MyCompressionCodecLocator implements Locator { @Override - public CompressionCodec resolveCompressionCodec(Header header) throws CompressionException { + public CompressionCodec locate(Header header) { - String alg = header.getCompressionAlgorithm(); + String id = header.getCompressionAlgorithm(); // 'zip' header - CompressionCodec codec = getCompressionCodec(alg); //implement me + CompressionCodec codec = getCompressionCodec(id); //implement me return codec; } } ``` +Your custom `Locator` can then inspect any other header as necessary. + +You then provide your custom `Locator` to the `JwtParserBuilder` as follows: + +```java +Locator myCodecLocator = new MyCompressionCodecLocator(); + +Jwts.parserBuilder() + + .setCompressionCodecLocator(myCodecLocator) // <---- + + // .. etc ... +``` + +Again, this is only necessary if the JWT-standard `zip` header lookup default behavior already supported by the +`JwtParser` is not sufficient. + ## JSON Support @@ -1265,9 +2659,11 @@ They are checked in order, and the first one found is used: 3. JSON-Java (`org.json`): This will be used automatically if you specify `io.jsonwebtoken:jjwt-orgjson` as a project runtime dependency. - **NOTE:** `org.json` APIs are natively enabled in Android environments so this is the recommended JSON processor for - Android applications _unless_ you want to use POJOs as claims. The `org.json` library supports simple - Object-to-JSON marshaling, but it *does not* support JSON-to-Object unmarshalling. + > **Note** + > + > `org.json` APIs are natively enabled in Android environments so this is the recommended JSON processor for + > Android applications _unless_ you want to use POJOs as claims. The `org.json` library supports simple + > Object-to-JSON marshaling, but it *does not* support JSON-to-Object unmarshalling. **If you want to use POJOs as claim values, use either the `io.jsonwebtoken:jjwt-jackson` or `io.jsonwebtoken:jjwt-gson` dependency** (or implement your own Serializer and Deserializer if desired). **But beware**, @@ -1328,7 +2724,7 @@ scope which is the typical JJWT default). That is: io.jsonwebtoken jjwt-jackson - 0.11.5 + JJWT_RELEASE_VERSION compile ``` @@ -1337,7 +2733,7 @@ scope which is the typical JJWT default). That is: ```groovy dependencies { - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:JJWT_RELEASE_VERSION' } ``` @@ -1374,7 +2770,8 @@ By default JJWT will only convert simple claim types: String, Date, Long, Intege new JacksonDeserializer(Maps.of("user", User.class).build()) ``` -This would trigger the value in the `user` claim to be deserialized into the custom type of `User`. Given the claims body of: +This would trigger the value in the `user` claim to be deserialized into the custom type of `User`. Given the claims +payload of: ```json { @@ -1397,15 +2794,17 @@ Jwts.parserBuilder() .parseClaimsJwt(aJwtString) - .getBody() + .getPayload() .get("user", User.class) // <----- ``` -**NOTE:** Using this constructor is mutually exclusive with the `JacksonDeserializer(ObjectMapper)` constructor -[described above](#json-jackson). This is because JJWT configures an `ObjectMapper` directly and could have negative -consequences for a shared `ObjectMapper` instance. This should work for most applications, if you need a more advanced -parsing options, [configure the mapper directly](#json-jackson). +> **Note** +> +> Using this constructor is mutually exclusive with the `JacksonDeserializer(ObjectMapper)` constructor +> [described above](#json-jackson). This is because JJWT configures an `ObjectMapper` directly and could have negative +> consequences for a shared `ObjectMapper` instance. This should work for most applications, if you need a more advanced +> parsing options, [configure the mapper directly](#json-jackson). ### Gson JSON Processor @@ -1421,11 +2820,15 @@ all that is required, no code or config is necessary. If you're curious, JJWT will automatically create an internal default Gson instance for its own needs as follows: ```java -new GsonBuilder().disableHtmlEscaping().create(); +new GsonBuilder() + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); ``` +The `registerTypeHierarchyAdapter` builder call is required to serialize JWKs with secret or private values. + However, if you prefer to use a different Gson instance instead of JJWT's default, you can configure JJWT to use your -own. +own - just don't forget to register the necessary JJWT type hierarchy adapter. You do this by declaring the `io.jsonwebtoken:jjwt-gson` dependency with **compile** scope (not runtime scope which is the typical JJWT default). That is: @@ -1436,7 +2839,7 @@ scope which is the typical JJWT default). That is: io.jsonwebtoken jjwt-gson - 0.11.5 + JJWT_RELEASE_VERSION compile ``` @@ -1445,7 +2848,7 @@ scope which is the typical JJWT default). That is: ```groovy dependencies { - implementation 'io.jsonwebtoken:jjwt-gson:0.11.5' + implementation 'io.jsonwebtoken:jjwt-gson:JJWT_RELEASE_VERSION' } ``` @@ -1453,7 +2856,10 @@ And then you can specify the `GsonSerializer` using your own `Gson` instance on ```java -Gson gson = getGson(); //implement me +Gson gson = new GsonBuilder() + // don't forget this line!: + .registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); String jws = Jwts.builder() @@ -1474,6 +2880,14 @@ Jwts.parser() // ... etc ... ``` +Again, as shown above, it is critical to create your `Gson` instance using the `GsonBuilder` and include the line: + +```java +.registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, GsonSupplierSerializer.INSTANCE) +``` + +to ensure JWK serialization works as expected. + ## Base64 Support @@ -1524,9 +2938,8 @@ underlying raw byte array data. No key or secret is required to decode Base64 t Based on this, when encoding sensitive byte data with Base64 - like a shared or private key - **the resulting string is NOT safe to expose publicly**. -A base64-encoded key is still sensitive information and must -be kept as secret and as safe as the original thing you got the bytes from (e.g. a Java `PrivateKey` or `SecretKey` -instance). +A base64-encoded key is still sensitive information and must be kept as secret and as safe as the original source +of the bytes (e.g. a Java `PrivateKey` or `SecretKey` instance). After Base64-encoding data into a string, it is possible to then encrypt the string to keep it safe from prying eyes if desired, but this is different. Encryption is not encoding. They are separate concepts. @@ -1548,7 +2961,7 @@ array. Going into the details of the Base64 algorithm is out of scope for this documentation, but there are many good Stackoverflow [answers](https://stackoverflow.com/questions/33663113/multiple-strings-base64-decoded-to-same-byte-array?noredirect=1&lq=1) -and [JJWT issue comments](https://github.com/jwtk/jjwt/issues/211#issuecomment-283076269) that explain this in detail. +and [JJWT issue comments](https://github.com/jwtk/jjwt/issues/211#issuecomment-283076269) that explain this in detail. Here's one [good answer](https://stackoverflow.com/questions/29941270/why-do-base64-decode-produce-same-byte-array-for-different-strings): > Remember that Base64 encodes each 8 bit entity into 6 bit chars. The resulting string then needs exactly @@ -1621,6 +3034,507 @@ Jwts.parserBuilder() // ... etc ... ``` + +## Examples + +* [JWS Signed with HMAC](#example-jws-hs) +* [JWS Signed with RSA](#example-jws-rsa) +* [JWS Signed with ECDSA](#example-jws-ecdsa) +* [JWS Signed with EdDSA](#example-jws-eddsa) +* [JWE Encrypted Directly with a SecretKey](#example-jwe-dir) +* [JWE Encrypted with RSA](#example-jwe-rsa) +* [JWE Encrypted with AES Key Wrap](#example-jwe-aeskw) +* [JWE Encrypted with ECDH-ES](#example-jwe-ecdhes) +* [JWE Encrypted with a Password](#example-jwe-password) +* [SecretKey JWK](#example-jwk-secret) +* [RSA Public JWK](#example-jwk-rsapub) +* [RSA Private JWK](#example-jwk-rsapriv) +* [Elliptic Curve Public JWK](#example-jwk-ecpub) +* [Elliptic Curve Private JWK](#example-jwk-ecpriv) +* [Edwards Elliptic Curve Public JWK](#example-jwk-edpub) +* [Edwards Elliptic Curve Private JWK](#example-jwk-edpriv) + + +### JWT Signed with HMAC + +This is an example showing how to digitally sign a JWT using an [HMAC](https://en.wikipedia.org/wiki/HMAC) +(hash-based message authentication code). The JWT specifications define 3 standard HMAC signing algorithms: + +* `HS256`: HMAC with SHA-256. This requires a 256-bit (32 byte) `SecretKey` or larger. +* `HS384`: HMAC with SHA-384. This requires a 384-bit (48 byte) `SecretKey` or larger. +* `HS512`: HMAC with SHA-512. This requires a 512-bit (64 byte) `SecretKey` or larger. + +Example: + +```java +// Create a test key suitable for the desired HMAC-SHA algorithm: +MacAlgorithm alg = Jwts.SIG.HS512; //or HS384 or HS256 +SecretKey key = alg.keyBuilder().build(); + +String message = "Hello World!"; +byte[] content = message.getBytes(StandardCharsets.UTF_8); + +// Create the compact JWS: +String jws = Jwts.builder().setContent(content, "text/plain").signWith(key, alg).compact(); + +// Parse the compact JWS: +content = Jwts.parserBuilder().verifyWith(key).build().parseContentJws(jws).getPayload(); + +assert message.equals(new String(content, StandardCharsets.UTF_8)); +``` + + +### JWT Signed with RSA + +This is an example showing how to digitally sign and verify a JWT using RSA cryptography. The JWT specifications +define [6 standard RSA signing algorithms](#jws-alg). All 6 require that [RSA keys 2048-bits or larger](#jws-key-rsa) +must be used. + +In this example, Bob will sign a JWT using his RSA private key, and Alice can verify it came from Bob using Bob's RSA +public key: + +```java +// Create a test key suitable for the desired RSA signature algorithm: +SignatureAlgorithm alg = Jwts.SIG.RS512; //or PS512, RS256, etc... +KeyPair pair = alg.keyPairBuilder().build(); + +// Bob creates the compact JWS with his RSA private key: +String jws = Jwts.builder().setSubject("Alice") + .signWith(pair.getPrivate(), alg) // <-- Bob's RSA private key + .compact(); + +// Alice receives and verifies the compact JWS came from Bob: +String subject = Jwts.parserBuilder() + .verifyWith(pair.getPublic()) // <-- Bob's RSA public key + .build().parseClaimsJws(jws).getPayload().getSubject(); + +assert "Alice".equals(subject); +``` + + +### JWT Signed with ECDSA + +This is an example showing how to digitally sign and verify a JWT using the Elliptic Curve Digital Signature Algorithm. +The JWT specifications define [3 standard ECDSA signing algorithms](#jws-alg): + +* `ES256`: ECDSA using P-256 and SHA-256. This requires an EC Key exactly 256 bits (32 bytes) long. +* `ES384`: ECDSA using P-384 and SHA-384. This requires an EC Key exactly 384 bits (48 bytes) long. +* `ES512`: ECDSA using P-521 and SHA-512. This requires an EC Key exactly 521 bits (65 or 66 bytes depending on format) long. + +In this example, Bob will sign a JWT using his EC private key, and Alice can verify it came from Bob using Bob's EC +public key: + +```java +// Create a test key suitable for the desired ECDSA signature algorithm: +SignatureAlgorithm alg = Jwts.SIG.ES512; //or ES256 or ES384 +KeyPair pair = alg.keyPairBuilder().build(); + +// Bob creates the compact JWS with his EC private key: +String jws = Jwts.builder().setSubject("Alice") + .signWith(pair.getPrivate(), alg) // <-- Bob's EC private key + .compact(); + +// Alice receives and verifies the compact JWS came from Bob: +String subject = Jwts.parserBuilder() + .verifyWith(pair.getPublic()) // <-- Bob's EC public key + .build().parseClaimsJws(jws).getPayload().getSubject(); + +assert "Alice".equals(subject); +``` + + +### JWT Signed with EdDSA + +This is an example showing how to digitally sign and verify a JWT using the +[Edwards Curve Digital Signature Algorithm](https://www.rfc-editor.org/rfc/rfc8032) using +`Ed25519` or `Ed448` keys. + +> **Note** +> +> **The `Ed25519` and `Ed448` algorithms require JDK 15 or a compatible JCA Provider +> (like BouncyCastle) in the runtime classpath.** +> +> If you are using JDK 14 or earlier and you want to use them, see +> the [Installation](#Installation) section to see how to enable BouncyCastle. + +The `EdDSA` signature algorithm is defined for JWS in [RFC 8037, Section 3.1](https://www.rfc-editor.org/rfc/rfc8037#section-3.1) +using keys for two Edwards curves: + +* `Ed25519`: `EdDSA` using curve `Ed25519`. `Ed25519` algorithm keys must be 256 bits (32 bytes) long and produce + signatures 512 bits (64 bytes) long. +* `Ed448`: `EdDSA` using curve `Ed448`. `Ed448` algorithm keys must be 456 bits (57 bytes) long and produce signatures + 912 bits (114 bytes) long. + +In this example, Bob will sign a JWT using his Edwards Curve private key, and Alice can verify it came from Bob +using Bob's Edwards Curve public key: + +```java +// Create a test key suitable for the EdDSA signature algorithm using Ed25519 or Ed448 keys: +SignatureAlgorithm alg = Jwts.SIG.Ed25519; //or Ed448 +KeyPair pair = alg.keyPairBuilder().build(); + +// Bob creates the compact JWS with his Edwards Curve private key: +String jws = Jwts.builder().setSubject("Alice") + .signWith(pair.getPrivate(), alg) // <-- Bob's Edwards Curve private key + .compact(); + +// Alice receives and verifies the compact JWS came from Bob: +String subject = Jwts.parserBuilder() + .verifyWith(pair.getPublic()) // <-- Bob's Edwards Curve public key + .build().parseClaimsJws(jws).getPayload().getSubject(); + +assert "Alice".equals(subject); +``` + + +### JWT Encrypted Directly with a SecretKey + +This is an example showing how to encrypt a JWT [directly using a symmetric secret key](#jwe-alg-dir). The +JWT specifications define [6 standard AEAD Encryption algorithms](#jwe-enc): + +* `A128GCM`: AES GCM using a 128-bit (16 byte) `SecretKey` or larger. +* `A192GCM`: AES GCM using a 192-bit (24 byte) `SecretKey` or larger. +* `A256GCM`: AES GCM using a 256-bit (32 byte) `SecretKey` or larger. +* `A128CBC-HS256`: [AES_128_CBC_HMAC_SHA_256](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.3) using a + 256-bit (32 byte) `SecretKey`. +* `A192CBC-HS384`: [AES_192_CBC_HMAC_SHA_384](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.4) using a + 384-bit (48 byte) `SecretKey`. +* `A256CBC-HS512`: [AES_256_CBC_HMAC_SHA_512](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5) using a + 512-bit (64 byte) `SecretKey`. + +The AES GCM (`A128GCM`, `A192GCM` and `A256GCM`) algorithms are strongly recommended - they are faster and more +efficient than the `A*CBC-HS*` variants, but they do require JDK 8 or later (or JDK 7 + BouncyCastle). + +Example: + +```java +// Create a test key suitable for the desired payload encryption algorithm: +// (A*GCM algorithms are recommended, but require JDK >= 8 or BouncyCastle) +AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A128GCM, A192GCM, A256CBC-HS512, etc... +SecretKey key = enc.keyBuilder().build(); + +String message = "Live long and prosper."; +byte[] content = message.getBytes(StandardCharsets.UTF_8); + +// Create the compact JWE: +String jwe = Jwts.builder().setContent(content, "text/plain").encryptWith(key, enc).compact(); + +// Parse the compact JWE: +content = Jwts.parserBuilder().decryptWith(key).build().parseContentJwe(jwe).getPayload(); + +assert message.equals(new String(content, StandardCharsets.UTF_8)); +``` + + +### JWT Encrypted with RSA + +This is an example showing how to encrypt and decrypt a JWT using RSA cryptography. + +Because RSA cannot encrypt much data, RSA is used to encrypt and decrypt a secure-random key, and that generated key +in turn is used to actually encrypt the payload as described in the [RSA Key Encryption](jwe-alg-rsa) section +above. As such, RSA Key Algorithms must be paired with an AEAD Encryption Algorithm, as shown below. + +In this example, Bob will encrypt a JWT using Alice's RSA public key to ensure only she may read it. Alice can then +decrypt the JWT using her RSA private key: + +```java +// Create a test KeyPair suitable for the desired RSA key algorithm: +KeyPair pair = Jwts.SIG.RS512.keyPairBuilder().build(); + +// Choose the key algorithm used encrypt the payload key: +KeyAlgorithm alg = Jwts.KEY.RSA_OAEP_256; //or RSA_OAEP or RSA1_5 +// Choose the Encryption Algorithm to encrypt the payload: +AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + +// Bob creates the compact JWE with Alice's RSA public key so only she may read it: +String jwe = Jwts.builder().setAudience("Alice") + .encryptWith(pair.getPublic(), alg, enc) // <-- Alice's RSA public key + .compact(); + +// Alice receives and decrypts the compact JWE: +String audience = Jwts.parserBuilder() + .decryptWith(pair.getPrivate()) // <-- Alice's RSA private key + .build().parseClaimsJwe(jwe).getPayload().getAudience(); + +assert "Alice".equals(audience); +``` + + +### JWT Encrypted with AES Key Wrap + +This is an example showing how to encrypt and decrypt a JWT using AES Key Wrap algorithms. + +These algorithms use AES to encrypt and decrypt a secure-random key, and that generated key in turn is used to actually encrypt +the payload as described in the [AES Key Encryption](jwe-alg-aes) section above. This allows the payload to be +encrypted with a random short-lived key, reducing material exposure of the potentially longer-lived symmetric secret +key. This approach requires the AES Key Wrap algorithms to be paired with an AEAD content encryption algorithm, +as shown below. + +The AES GCM Key Wrap algorithms (`A128GCMKW`, `A192GCMKW` and `A256GCMKW`) are preferred - they are faster and more +efficient than the `A*KW` variants, but they do require JDK 8 or later (or JDK 7 + BouncyCastle). + +```java +// Create a test SecretKey suitable for the desired AES Key Wrap algorithm: +SecretKeyAlgorithm alg = Jwts.KEY.A256GCMKW; //or A192GCMKW, A128GCMKW, A256KW, etc... +SecretKey key = alg.keyBuilder().build(); + +// Chooose the Encryption Algorithm used to encrypt the payload: +AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + +// Create the compact JWE: +String jwe = Jwts.builder().setIssuer("me").encryptWith(key, alg, enc).compact(); + +// Parse the compact JWE: +String issuer = Jwts.parserBuilder().decryptWith(key).build() + .parseClaimsJwe(jwe).getPayload().getIssuer(); + +assert "me".equals(issuer); +``` + + +### JWT Encrypted with ECDH-ES + +This is an example showing how to encrypt and decrypt a JWT using Elliptic Curve Diffie-Hellman Ephmeral Static +Key Agreement (ECDH-ES) algorithms. + +These algorithms use ECDH-ES to encrypt and decrypt a secure-random key, and that +generated key in turn is used to actually encrypt the payload as described in the +[Elliptic Curve Diffie-Hellman Ephemeral Static Key Agreement](jwe-alg-ecdhes) section above. Because of this, ECDH-ES +Key Algorithms must be paired with an AEAD Encryption Algorithm, as shown below. + +In this example, Bob will encrypt a JWT using Alice's Elliptic Curve public key to ensure only she may read it. +Alice can then decrypt the JWT using her Elliptic Curve private key: + +```java +// Create a test KeyPair suitable for the desired EC key algorithm: +KeyPair pair = Jwts.SIG.ES512.keyPairBuilder().build(); + +// Choose the key algorithm used encrypt the payload key: +KeyAlgorithm alg = Jwts.KEY.ECDH_ES_A256KW; //ECDH_ES_A192KW, etc. +// Choose the Encryption Algorithm to encrypt the payload: +AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + +// Bob creates the compact JWE with Alice's EC public key so only she may read it: +String jwe = Jwts.builder().setAudience("Alice") + .encryptWith(pair.getPublic(), alg, enc) // <-- Alice's EC public key + .compact(); + +// Alice receives and decrypts the compact JWE: +String audience = Jwts.parserBuilder() + .decryptWith(pair.getPrivate()) // <-- Alice's EC private key + .build().parseClaimsJwe(jwe).getPayload().getAudience(); + +assert "Alice".equals(audience); +``` + + +### JWT Encrypted with a Password + +This is an example showing how to encrypt and decrypt a JWT using Password-based key-derivation algorithms. + +These algorithms use a password to securely derive a random key, and that derived random key in turn is used to actually +encrypt the payload as described in the [Password-based Key Encryption](jwe-alg-pbes2) section above. This allows +the payload to be encrypted with a random short-lived cryptographically-stronger key, reducing the need to +expose the longer-lived (and potentially weaker) password. + +This approach requires the Password-based Key Wrap algorithms to be paired with an AEAD content encryption algorithm, +as shown below. + +```java +//DO NOT use this example password in a real app, it is well-known to password crackers: +String pw = "correct horse battery staple"; +Password password = Keys.forPassword(pw.toCharArray()); + +// Choose the desired PBES2 key derivation algorithm: +KeyAlgorithm alg = Jwts.KEY.PBES2_HS512_A256KW; //or PBES2_HS384_A192KW or PBES2_HS256_A128KW + +// Optionally choose the number of PBES2 computational iterations to use to derive the key. +// This is optional - if you do not specify a value, JJWT will automatically choose a value +// based on your chosen PBES2 algorithm and OWASP PBKDF2 recommendations here: +// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 +// +// If you do specify a value, ensure the iterations are large enough for your desired alg +//int pbkdf2Iterations = 120000; //for HS512. Needs to be much higher for smaller hash algs. + +// Choose the Encryption Algorithm used to encrypt the payload: +AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + +// Create the compact JWE: +String jwe = Jwts.builder().setIssuer("me") + // Optional work factor is specified in the header: + //.setHeader(Jwts.headerBuilder().setPbes2Count(pbkdf2Iterations)) + .encryptWith(password, alg, enc) + .compact(); + +// Parse the compact JWE: +String issuer = Jwts.parserBuilder().decryptWith(password) + .build().parseClaimsJwe(jwe).getPayload().getIssuer(); + +assert "me".equals(issuer); +``` + + +### SecretKey JWK + +Example creating and parsing a secret JWK: + +```java +SecretKey key = Jwts.SIG.HS512.keyBuilder().build(); // or HS384 or HS256 +SecretJwk jwk = Jwks.builder().forKey(key).setIdFromThumbprint().build(); + +assert jwk.getId().equals(jwk.thumbprint().toString()); +assert key.equals(jwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof SecretJwk; +assert jwk.equals(parsed); +``` + + +### RSA Public JWK + +Example creating and parsing an RSA Public JWK: + +```java +RSAPublicKey key = (RSAPublicKey)Jwts.SIG.RS512.keyPairBuilder().build().getPublic(); +RsaPublicJwk jwk = Jwks.builder().forKey(key).setIdFromThumbprint().build(); + +assert jwk.getId().equals(jwk.thumbprint().toString()); +assert key.equals(jwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof RsaPublicJwk; +assert jwk.equals(parsed); +``` + + +### RSA Private JWK + +Example creating and parsing an RSA Private JWK: + +```java +KeyPair pair = Jwts.SIG.RS512.keyPairBuilder().build(); +RSAPublicKey pubKey = (RSAPublicKey) pair.getPublic(); +RSAPrivateKey privKey = (RSAPrivateKey) pair.getPrivate(); + +RsaPrivateJwk privJwk = Jwks.builder().forKey(privKey).setIdFromThumbprint().build(); +RsaPublicJwk pubJwk = privJwk.toPublicJwk(); + +assert privJwk.getId().equals(privJwk.thumbprint().toString()); +assert pubJwk.getId().equals(pubJwk.thumbprint().toString()); +assert privKey.equals(privJwk.toKey()); +assert pubKey.equals(pubJwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(privJwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof RsaPrivateJwk; +assert privJwk.equals(parsed); +``` + + +### Elliptic Curve Public JWK + +Example creating and parsing an Elliptic Curve Public JWK: + +```java +ECPublicKey key = (ECPublicKey) Jwts.SIG.ES512.keyPairBuilder().build().getPublic(); +EcPublicJwk jwk = Jwks.builder().forKey(key).setIdFromThumbprint().build(); + +assert jwk.getId().equals(jwk.thumbprint().toString()); +assert key.equals(jwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof EcPublicJwk; +assert jwk.equals(parsed); +``` + + +### Elliptic Curve Private JWK + +Example creating and parsing an Elliptic Curve Private JWK: + +```java +KeyPair pair = Jwts.SIG.ES512.keyPairBuilder().build(); +ECPublicKey pubKey = (ECPublicKey) pair.getPublic(); +ECPrivateKey privKey = (ECPrivateKey) pair.getPrivate(); + +EcPrivateJwk privJwk = Jwks.builder().forKey(privKey).setIdFromThumbprint().build(); +EcPublicJwk pubJwk = privJwk.toPublicJwk(); + +assert privJwk.getId().equals(privJwk.thumbprint().toString()); +assert pubJwk.getId().equals(pubJwk.thumbprint().toString()); +assert privKey.equals(privJwk.toKey()); +assert pubKey.equals(pubJwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(privJwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof EcPrivateJwk; +assert privJwk.equals(parsed); +``` + + +### Edwards Elliptic Curve Public JWK + +Example creating and parsing an Edwards Elliptic Curve (Ed25519, Ed448, X25519, X448) Public JWK +(the JWT [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) specification calls these `Octet` keys, hence the +`OctetPublicJwk` interface names): + +```java +PublicKey key = Jwts.SIG.Ed25519.keyPairBuilder().build().getPublic(); +OctetPublicJwk jwk = builder().forOctetKey(key).setIdFromThumbprint().build(); + +assert jwk.getId().equals(jwk.thumbprint().toString()); +assert key.equals(jwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof OctetPublicJwk; +assert jwk.equals(parsed); +``` + + +### Edwards Elliptic Curve Private JWK + +Example creating and parsing an Edwards Elliptic Curve (Ed25519, Ed448, X25519, X448) Private JWK +(the JWT [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) specification calls these `Octet` keys, hence the +`OctetPrivateJwk` and `OctetPublicJwk` interface names): + +```java +KeyPair pair = Jwts.SIG.Ed448.keyPairBuilder().build(); +PublicKey pubKey = pair.getPublic(); +PrivateKey privKey = pair.getPrivate(); + +OctetPrivateJwk privJwk = builder().forOctetKey(privKey).setIdFromThumbprint().build(); +OctetPublicJwk pubJwk = privJwk.toPublicJwk(); + +assert privJwk.getId().equals(privJwk.thumbprint().toString()); +assert pubJwk.getId().equals(pubJwk.thumbprint().toString()); +assert privKey.equals(privJwk.toKey()); +assert pubKey.equals(pubJwk.toKey()); + +byte[] utf8Bytes = new JacksonSerializer().serialize(privJwk); // or GsonSerializer(), etc +String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); +Jwk parsed = Jwks.parser().build().parse(jwkJson); + +assert parsed instanceof OctetPrivateJwk; +assert privJwk.equals(parsed); +``` + ## Learn More - [JSON Web Token for Java and Android](https://stormpath.com/blog/jjwt-how-it-works-why/) @@ -1632,7 +3546,7 @@ Jwts.parserBuilder() ## Author -Maintained by Les Hazlewood & the community :heart: +Maintained by Les Hazlewood & the extended Java community :heart: ## License diff --git a/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java b/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java index bb7c81fd..420a093d 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimJwtException.java @@ -16,36 +16,83 @@ package io.jsonwebtoken; /** - * ClaimJwtException is a subclass of the {@link JwtException} that is thrown after a validation of an JTW claim failed. + * ClaimJwtException is a subclass of the {@link JwtException} that is thrown after a validation of an JWT claim failed. * * @since 0.5 */ public abstract class ClaimJwtException extends JwtException { + /** + * Deprecated as this is an implementation detail accidentally exposed in the JJWT 0.5 public API. It is no + * longer referenced anywhere in JJWT's implementation and will be removed in a future release. + * + * @deprecated will be removed in a future release. + */ + @Deprecated public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + + /** + * Deprecated as this is an implementation detail accidentally exposed in the JJWT 0.5 public API. It is no + * longer referenced anywhere in JJWT's implementation and will be removed in a future release. + * + * @deprecated will be removed in a future release. + */ + @Deprecated public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not present in the JWT claims."; - private final Header header; + /** + * The header associated with the Claims that failed validation. + */ + private final Header header; + /** + * The Claims that failed validation. + */ private final Claims claims; - protected ClaimJwtException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified header, claims and exception message. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + */ + protected ClaimJwtException(Header header, Claims claims, String message) { super(message); this.header = header; this.claims = claims; } - protected ClaimJwtException(Header header, Claims claims, String message, Throwable cause) { + /** + * Creates a new instance with the specified header, claims and exception message as a result of encountering + * the specified {@code cause}. + * + * @param header the header inspected + * @param claims the claims obtained + * @param message the exception message + * @param cause the exception that caused this ClaimJwtException to be thrown. + */ + protected ClaimJwtException(Header header, Claims claims, String message, Throwable cause) { super(message, cause); this.header = header; this.claims = claims; } + /** + * Returns the {@link Claims} that failed validation. + * + * @return the {@link Claims} that failed validation. + */ public Claims getClaims() { return claims; } - public Header getHeader() { + /** + * Returns the header associated with the {@link #getClaims() claims} that failed validation. + * + * @return the header associated with the {@link #getClaims() claims} that failed validation. + */ + public Header getHeader() { return header; } } diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index 36f50ba3..9311e590 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -24,12 +24,11 @@ import java.util.Map; *

This is ultimately a JSON map and any values can be added to it, but JWT standard names are provided as * type-safe getters and setters for convenience.

* - *

Because this interface extends {@code Map<String, Object>}, if you would like to add your own properties, + *

Because this interface extends Map<String, Object>, if you would like to add your own properties, * you simply use map methods, for example:

* - *
- * claims.{@link Map#put(Object, Object) put}("someKey", "someValue");
- * 
+ *
+ * claims.{@link Map#put(Object, Object) put}("someKey", "someValue");
* *

Creation

* @@ -41,25 +40,25 @@ import java.util.Map; public interface Claims extends Map, ClaimsMutator { /** JWT {@code Issuer} claims parameter name: "iss" */ - public static final String ISSUER = "iss"; + String ISSUER = "iss"; /** JWT {@code Subject} claims parameter name: "sub" */ - public static final String SUBJECT = "sub"; + String SUBJECT = "sub"; /** JWT {@code Audience} claims parameter name: "aud" */ - public static final String AUDIENCE = "aud"; + String AUDIENCE = "aud"; /** JWT {@code Expiration} claims parameter name: "exp" */ - public static final String EXPIRATION = "exp"; + String EXPIRATION = "exp"; /** JWT {@code Not Before} claims parameter name: "nbf" */ - public static final String NOT_BEFORE = "nbf"; + String NOT_BEFORE = "nbf"; /** JWT {@code Issued At} claims parameter name: "iat" */ - public static final String ISSUED_AT = "iat"; + String ISSUED_AT = "iat"; /** JWT {@code JWT ID} claims parameter name: "jti" */ - public static final String ID = "jti"; + String ID = "jti"; /** * Returns the JWT diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 66528d8f..34333d99 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -25,7 +25,7 @@ import java.util.Date; * @see io.jsonwebtoken.Claims * @since 0.2 */ -public interface ClaimsMutator { +public interface ClaimsMutator> { /** * Sets the JWT diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java index 47e54761..450d874d 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -18,38 +18,47 @@ package io.jsonwebtoken; /** * Compresses and decompresses byte arrays according to a compression algorithm. * + *

"zip" identifier

+ * + *

{@code CompressionCodec} extends {@code Identifiable}; the value returned from + * {@link Identifiable#getId() getId()} will be used as the JWT + * zip header value.

+ * * @see CompressionCodecs#DEFLATE * @see CompressionCodecs#GZIP * @since 0.6.0 */ -public interface CompressionCodec { +public interface CompressionCodec extends Identifiable { /** - * The compression algorithm name to use as the JWT's {@code zip} header value. + * The algorithm name to use as the JWT + * zip header value. * - * @return the compression algorithm name to use as the JWT's {@code zip} header value. + * @return the algorithm name to use as the JWT + * zip header value. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link Identifiable#getId()} to ensure congruence with + * all other identifiable algorithms. */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated String getAlgorithmName(); /** - * Compresses the specified byte array according to the compression {@link #getAlgorithmName() algorithm}. + * Compresses the specified byte array, returning the compressed byte array result. * - * @param payload bytes to compress + * @param content bytes to compress * @return compressed bytes - * @throws CompressionException if the specified byte array cannot be compressed according to the compression - * {@link #getAlgorithmName() algorithm}. + * @throws CompressionException if the specified byte array cannot be compressed. */ - byte[] compress(byte[] payload) throws CompressionException; + byte[] compress(byte[] content) throws CompressionException; /** - * Decompresses the specified compressed byte array according to the compression - * {@link #getAlgorithmName() algorithm}. The specified byte array must already be in compressed form - * according to the {@link #getAlgorithmName() algorithm}. + * 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 according to the compression - * {@link #getAlgorithmName() algorithm}. + * @throws CompressionException if the specified byte array cannot be decompressed. */ byte[] decompress(byte[] compressed) throws CompressionException; } \ No newline at end of file diff --git a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 50ebe08f..ba61ff55 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -30,7 +30,11 @@ package io.jsonwebtoken; * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

* * @since 0.6.0 + * @deprecated in favor of {@link Locator} + * @see JwtParserBuilder#setCompressionCodecLocator(Locator) */ +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated public interface CompressionCodecResolver { /** @@ -41,6 +45,6 @@ public interface CompressionCodecResolver { * @return CompressionCodec matching the {@code zip} header, or null if there is no {@code zip} header. * @throws CompressionException if a {@code zip} header value is found and not supported. */ - CompressionCodec resolveCompressionCodec(Header header) throws CompressionException; + CompressionCodec resolveCompressionCodec(Header header) throws CompressionException; } diff --git a/api/src/main/java/io/jsonwebtoken/CompressionException.java b/api/src/main/java/io/jsonwebtoken/CompressionException.java index 287ccfb0..fd2c045c 100644 --- a/api/src/main/java/io/jsonwebtoken/CompressionException.java +++ b/api/src/main/java/io/jsonwebtoken/CompressionException.java @@ -15,17 +15,30 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.io.IOException; + /** - * Exception indicating that either compressing or decompressing an JWT body failed. + * Exception indicating that either compressing or decompressing a JWT body failed. * * @since 0.6.0 */ -public class CompressionException extends JwtException { +public class CompressionException extends IOException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public CompressionException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public CompressionException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/DynamicHeaderBuilder.java b/api/src/main/java/io/jsonwebtoken/DynamicHeaderBuilder.java new file mode 100644 index 00000000..139985fc --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/DynamicHeaderBuilder.java @@ -0,0 +1,39 @@ +/* + * 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; + +import io.jsonwebtoken.lang.Builder; +import io.jsonwebtoken.lang.MapMutator; +import io.jsonwebtoken.security.X509Builder; + +/** + * A {@link Builder} that dynamically determines the type of {@link Header} to create based on builder state. + *
    + *
  • If only standard {@link Header} properties have been set (that is, no + * {@link JwsHeader} or {@link JweHeader} properties have been set), an {@link UnprotectedHeader} will be created.
  • + *
  • If any {@link ProtectedHeader} properties have been set (but no {@link JweHeader} properties), a + * {@link JwsHeader} will be created.
  • + *
  • If any {@link JweHeader} properties have been set, a {@link JweHeader} will be created.
  • + *
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface DynamicHeaderBuilder extends + MapMutator, + X509Builder, + JweHeaderMutator, + Builder> { +} diff --git a/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java b/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java index 0748ca36..f3683db7 100644 --- a/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/ExpiredJwtException.java @@ -22,18 +22,27 @@ package io.jsonwebtoken; */ public class ExpiredJwtException extends ClaimJwtException { - public ExpiredJwtException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified header, claims, and explanation message. + * + * @param header jwt header + * @param claims jwt claims (body) + * @param message the message explaining why the exception is thrown. + */ + public ExpiredJwtException(Header header, Claims claims, String message) { super(header, claims, message); } /** - * @param header jwt header - * @param claims jwt claims (body) - * @param message exception message - * @param cause cause + * Creates a new instance with the specified header, claims, explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + * @param header jwt header + * @param claims jwt claims (body) * @since 0.5 */ - public ExpiredJwtException(Header header, Claims claims, String message, Throwable cause) { + public ExpiredJwtException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/Header.java b/api/src/main/java/io/jsonwebtoken/Header.java index 7365d951..a25df648 100644 --- a/api/src/main/java/io/jsonwebtoken/Header.java +++ b/api/src/main/java/io/jsonwebtoken/Header.java @@ -32,103 +32,122 @@ import java.util.Map; * *

Creation

* - *

It is easiest to create a {@code Header} instance by calling one of the - * {@link Jwts#header() JWTs.header()} factory methods.

+ *

It is easiest to create a {@code Header} instance by using {@link Jwts#header()}.

* * @since 0.1 */ -public interface Header> extends Map { - - /** JWT {@code Type} (typ) value: "JWT" */ - public static final String JWT_TYPE = "JWT"; - - /** JWT {@code Type} header parameter name: "typ" */ - public static final String TYPE = "typ"; - - /** JWT {@code Content Type} header parameter name: "cty" */ - public static final String CONTENT_TYPE = "cty"; - - /** JWT {@code Compression Algorithm} header parameter name: "zip" */ - public static final String COMPRESSION_ALGORITHM = "zip"; - - /** JJWT legacy/deprecated compression algorithm header parameter name: "calg" - * @deprecated use {@link #COMPRESSION_ALGORITHM} instead. */ - @Deprecated - public static final String DEPRECATED_COMPRESSION_ALGORITHM = "calg"; +public interface Header> extends Map, HeaderMutator { /** - * Returns the - * typ (type) header value or {@code null} if not present. + * JWT {@code Type} (typ) value: "JWT" + * + * @deprecated since JJWT_RELEASE_VERSION - this constant is never used within the JJWT codebase. + */ + @Deprecated + String JWT_TYPE = "JWT"; + + /** + * JWT {@code Type} header parameter name: "typ" + */ + String TYPE = "typ"; + + /** + * JWT {@code Content Type} header parameter name: "cty" + */ + String CONTENT_TYPE = "cty"; + + /** + * JWT {@code Algorithm} header parameter name: "alg". + * + * @see JWS Algorithm Header + * @see JWE Algorithm Header + */ + String ALGORITHM = "alg"; + + /** + * JWT {@code Compression Algorithm} header parameter name: "zip" + */ + String COMPRESSION_ALGORITHM = "zip"; + + /** + * JJWT legacy/deprecated compression algorithm header parameter name: "calg" + * + * @deprecated use {@link #COMPRESSION_ALGORITHM} instead. + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + String DEPRECATED_COMPRESSION_ALGORITHM = "calg"; + + /** + * Returns the + * typ (Type) header value or {@code null} if not present. * * @return the {@code typ} header value or {@code null} if not present. */ String getType(); /** - * Sets the JWT - * typ (Type) header value. A {@code null} value will remove the property from the JSON map. + * Returns the + * cty (Content Type) header value or {@code null} if not present. * - * @param typ the JWT JOSE {@code typ} header value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. - */ - T setType(String typ); - - /** - * Returns the - * cty (Content Type) header value or {@code null} if not present. + *

The cty (Content Type) Header Parameter is used by applications to declare the + * IANA MediaType of the content + * (the payload). This is intended for use by the application when more than + * one kind of object could be present in the Payload; the application can use this value to disambiguate among + * the different kinds of objects that might be present. It will typically not be used by applications when + * the kind of object is already known. This parameter is ignored by JWT implementations (like JJWT); any + * processing of this parameter is performed by the JWS application. Use of this Header Parameter is OPTIONAL.

* - *

In the normal case where nested signing or encryption operations are not employed (i.e. a compact - * serialization JWT), the use of this header parameter is NOT RECOMMENDED. In the case that nested - * signing or encryption is employed, this Header Parameter MUST be present; in this case, the value MUST be - * {@code JWT}, to indicate that a Nested JWT is carried in this JWT. While media type names are not - * case-sensitive, it is RECOMMENDED that {@code JWT} always be spelled using uppercase characters for - * compatibility with legacy implementations. See - * JWT Appendix A.2 for - * an example of a Nested JWT.

+ *

To keep messages compact in common situations, it is RECOMMENDED that producers omit an + * application/ prefix of a media type value in a {@code 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 {@code cty} value not containing a + * '/'. For instance, a {@code 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".

* * @return the {@code typ} header parameter value or {@code null} if not present. */ String getContentType(); /** - * Sets the JWT - * cty (Content Type) header parameter value. A {@code null} value will remove the property from - * the JSON map. + * Returns the JWT {@code alg} (Algorithm) header value or {@code null} if not present. * - *

In the normal case where nested signing or encryption operations are not employed (i.e. a compact - * serialization JWT), the use of this header parameter is NOT RECOMMENDED. In the case that nested - * signing or encryption is employed, this Header Parameter MUST be present; in this case, the value MUST be - * {@code JWT}, to indicate that a Nested JWT is carried in this JWT. While media type names are not - * case-sensitive, it is RECOMMENDED that {@code JWT} always be spelled using uppercase characters for - * compatibility with legacy implementations. See - * JWT Appendix A.2 for - * an example of a Nested JWT.

+ *
    + *
  • If the JWT is a Signed JWT (a JWS), the + * alg (Algorithm) header parameter identifies the cryptographic algorithm used to secure the + * JWS. Consider using {@link Jwts#SIG}.{@link io.jsonwebtoken.lang.Registry#find(Object) find(id)} + * to convert this string value to a type-safe {@code SecureDigestAlgorithm} instance.
  • + *
  • If the JWT is an Encrypted JWT (a JWE), the + * alg (Algorithm) header parameter + * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content + * Encryption Key (CEK). The encrypted content is not usable if the alg value does not represent a + * supported algorithm, or if the recipient does not have a key that can be used with that algorithm. Consider + * using {@link Jwts#KEY}.{@link io.jsonwebtoken.lang.Registry#find(Object) find(id)} to convert this string value + * to a type-safe {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} instance.
  • + *
* - * @param cty the JWT JOSE {@code cty} header value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. + * @return the {@code alg} header value or {@code null} if not present. This will always be + * {@code non-null} on validly constructed JWT instances, but could be {@code null} during construction. + * @since JJWT_RELEASE_VERSION */ - T setContentType(String cty); + String getAlgorithm(); /** - * Returns the JWT zip (Compression Algorithm) header value or {@code null} if not present. + * Returns the JWT zip + * (Compression Algorithm) header parameter value or {@code null} if not present. + * + *

Compatibility Note

+ * + *

While the JWT family of specifications only defines the zip header in the JWE + * (JSON Web Encryption) specification, JJWT will also support compression for JWS as well if you choose to use it. + * However, be aware that if you use compression when creating a JWS token, other libraries may not be able to + * parse the JWS. However, compression when creating JWE tokens should be universally accepted for any library + * that supports JWE.

* * @return the {@code zip} header parameter value or {@code null} if not present. * @since 0.6.0 */ String getCompressionAlgorithm(); - - /** - * Sets the JWT zip (Compression Algorithm) header parameter value. A {@code null} value will remove - * the property from the JSON map. - * - *

The compression algorithm is NOT part of the JWT specification - * and must be used carefully since, is not expected that other libraries (including previous versions of this one) - * be able to deserialize a compressed JTW body correctly.

- * - * @param zip the JWT compression algorithm {@code zip} value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. - * @since 0.6.0 - */ - T setCompressionAlgorithm(String zip); - } diff --git a/api/src/main/java/io/jsonwebtoken/HeaderMutator.java b/api/src/main/java/io/jsonwebtoken/HeaderMutator.java new file mode 100644 index 00000000..0f6ec36f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/HeaderMutator.java @@ -0,0 +1,100 @@ +/* + * 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; + +/** + * Mutation (modifications) to a {@link Header Header} instance. + * + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface HeaderMutator> { + + /** + * Sets the JWT + * typ (Type) header value. A {@code null} value will remove the property from the JSON map. + * + * @param typ the JWT JOSE {@code typ} header value or {@code null} to remove the property from the JSON map. + * @return the {@code Header} instance for method chaining. + */ + T setType(String typ); + + /** + * Sets the JWT + * cty (Content Type) header parameter value. A {@code null} value will remove the property from + * the JSON map. + * + *

The cty (Content Type) Header Parameter is used by applications to declare the + * IANA MediaType of the content + * (the payload). This is intended for use by the application when more than + * one kind of object could be present in the Payload; the application can use this value to disambiguate among + * the different kinds of objects that might be present. It will typically not be used by applications when + * the kind of object is already known. This parameter is ignored by JWT implementations (like JJWT); any + * processing of this parameter is performed by the JWS application. Use of this Header Parameter is OPTIONAL.

+ * + *

To keep messages compact in common situations, it is RECOMMENDED that producers omit an + * application/ prefix of a media type value in a {@code 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 {@code cty} value not containing a + * '/'. For instance, a {@code 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".

+ * + * @param cty the JWT JOSE {@code cty} header value or {@code null} to remove the property from the JSON map. + * @return the {@code Header} instance for method chaining. + */ + T setContentType(String cty); + + /** + * Sets the JWT {@code alg} (Algorithm) header value. A {@code null} value will remove the property + * from the JSON map. + *
    + *
  • If the JWT is a Signed JWT (a JWS), the + * {@code alg} (Algorithm) header + * parameter identifies the cryptographic algorithm used to secure the JWS.
  • + *
  • If the JWT is an Encrypted JWT (a JWE), the + * alg (Algorithm) header parameter + * identifies the cryptographic key management algorithm used to encrypt or determine the value of the Content + * Encryption Key (CEK). The encrypted content is not usable if the alg value does not represent a + * supported algorithm, or if the recipient does not have a key that can be used with that algorithm.
  • + *
+ * + * @param alg the {@code alg} header value + * @return this header for method chaining + * @since JJWT_RELEASE_VERSION + */ + T setAlgorithm(String alg); + + /** + * Sets the JWT zip + * (Compression Algorithm) header parameter value. A {@code null} value will remove + * the property from the JSON map. + * + *

Compatibility Note

+ * + *

While the JWT family of specifications only defines the zip header in the JWE + * (JSON Web Encryption) specification, JJWT will also support compression for JWS as well if you choose to use it. + * However, be aware that if you use compression when creating a JWS token, other libraries may not be able to + * parse the JWS. However, Compression when creating JWE tokens should be universally accepted for any library + * that supports JWE.

+ * + * @param zip the JWT compression algorithm {@code zip} value or {@code null} to remove the property from the JSON map. + * @return the {@code Header} instance for method chaining. + * @since 0.6.0 + */ + T setCompressionAlgorithm(String zip); +} diff --git a/api/src/main/java/io/jsonwebtoken/Identifiable.java b/api/src/main/java/io/jsonwebtoken/Identifiable.java new file mode 100644 index 00000000..b7a10f3c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Identifiable.java @@ -0,0 +1,70 @@ +/* + * 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; + +/** + * An object that may be uniquely identified by an {@link #getId() id} relative to other instances of the same type. + * + *

All JWT concepts that have a + * JWA identifier value implement this interface. + * Specifically, there are four JWT concepts that are {@code Identifiable}. The following table indicates how + * their {@link #getId() id} values are used.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
JWA Identifiable Concepts
JJWT TypeHow {@link #getId()} is Used
{@link io.jsonwebtoken.security.SignatureAlgorithm SignatureAlgorithm}JWS protected header's + * {@code alg} (Algorithm) parameter value.
{@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}JWE protected header's + * {@code alg} (Key Management Algorithm) + * parameter value.
{@link io.jsonwebtoken.security.AeadAlgorithm AeadAlgorithm}JWE protected header's + * {@code enc} (Encryption Algorithm) + * parameter value.
{@link io.jsonwebtoken.security.Jwk Jwk}JWK's {@code kid} (Key ID) + * parameter value.
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface Identifiable { + + /** + * Returns the unique string identifier of the associated object. + * + * @return the unique string identifier of the associated object. + */ + String getId(); +} diff --git a/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java b/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java index df68fe1e..7667db23 100644 --- a/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/IncorrectClaimException.java @@ -23,11 +23,30 @@ package io.jsonwebtoken; */ public class IncorrectClaimException extends InvalidClaimException { - public IncorrectClaimException(Header header, Claims claims, String message) { - super(header, claims, message); + /** + * Creates a new instance with the specified header, claims and explanation message. + * + * @param header the header inspected + * @param claims the claims with the incorrect claim value + * @param claimName the name of the claim that could not be validated + * @param claimValue the value of the claim that could not be validated + * @param message the exception message + */ + public IncorrectClaimException(Header header, Claims claims, String claimName, Object claimValue, String message) { + super(header, claims, claimName, claimValue, message); } - public IncorrectClaimException(Header header, Claims claims, String message, Throwable cause) { - super(header, claims, message, cause); + /** + * Creates a new instance with the specified header, claims, explanation message and underlying cause. + * + * @param header the header inspected + * @param claims the claims with the incorrect claim value + * @param claimName the name of the claim that could not be validated + * @param claimValue the value of the claim that could not be validated + * @param message the exception message + * @param cause the underlying cause that resulted in this exception being thrown + */ + public IncorrectClaimException(Header header, Claims claims, String claimName, Object claimValue, String message, Throwable cause) { + super(header, claims, claimName, claimValue, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java index fbca6cab..54d6de79 100644 --- a/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -21,35 +21,66 @@ package io.jsonwebtoken; * * @see IncorrectClaimException * @see MissingClaimException - * * @since 0.6 */ public class InvalidClaimException extends ClaimJwtException { - private String claimName; - private Object claimValue; + /** + * The name of the invalid claim. + */ + private final String claimName; - protected InvalidClaimException(Header header, Claims claims, String message) { + /** + * The claim value that could not be validated. + */ + private final Object claimValue; + + /** + * Creates a new instance with the specified header, claims and explanation message. + * + * @param header the header inspected + * @param claims the claims obtained + * @param claimName the name of the claim that could not be validated + * @param claimValue the value of the claim that could not be validated + * @param message the exception message + */ + protected InvalidClaimException(Header header, Claims claims, String claimName, Object claimValue, String message) { super(header, claims, message); + this.claimName = claimName; + this.claimValue = claimValue; } - protected InvalidClaimException(Header header, Claims claims, String message, Throwable cause) { + /** + * Creates a new instance with the specified header, claims, explanation message and underlying cause. + * + * @param header the header inspected + * @param claims the claims obtained + * @param claimName the name of the claim that could not be validated + * @param claimValue the value of the claim that could not be validated + * @param message the exception message + * @param cause the underlying cause that resulted in this exception being thrown + */ + protected InvalidClaimException(Header header, Claims claims, String claimName, Object claimValue, String message, Throwable cause) { super(header, claims, message, cause); + this.claimName = claimName; + this.claimValue = claimValue; } + /** + * Returns the name of the invalid claim. + * + * @return the name of the invalid claim. + */ public String getClaimName() { return claimName; } - public void setClaimName(String claimName) { - this.claimName = claimName; - } - + /** + * Returns the claim value that could not be validated. + * + * @return the claim value that could not be validated. + */ public Object getClaimValue() { return claimValue; } - - public void setClaimValue(Object claimValue) { - this.claimValue = claimValue; - } } diff --git a/api/src/main/java/io/jsonwebtoken/Jwe.java b/api/src/main/java/io/jsonwebtoken/Jwe.java new file mode 100644 index 00000000..d0a0c36d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Jwe.java @@ -0,0 +1,42 @@ +/* + * 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; + +/** + * An encrypted JWT, called a "JWE", per the + * JWE (RFC 7516) Specification. + * + * @param payload type, either {@link Claims} or {@code byte[]} content. + * @since JJWT_RELEASE_VERSION + */ +public interface Jwe extends Jwt { + + /** + * Returns the Initialization Vector used during JWE encryption and decryption. + * + * @return the Initialization Vector used during JWE encryption and decryption. + */ + byte[] getInitializationVector(); + + /** + * Returns the Additional Authenticated Data authentication Tag used for JWE header + * authenticity and integrity verification. + * + * @return the Additional Authenticated Data authentication Tag used for JWE header + * authenticity and integrity verification. + */ + byte[] getAadTag(); +} diff --git a/api/src/main/java/io/jsonwebtoken/JweHeader.java b/api/src/main/java/io/jsonwebtoken/JweHeader.java new file mode 100644 index 00000000..24f67e3d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/JweHeader.java @@ -0,0 +1,171 @@ +/* + * 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; + +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.StandardKeyAlgorithms; + +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * A JWE header. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JweHeader extends ProtectedHeader, JweHeaderMutator { + + /** + * Returns the JWE {@code enc} (Encryption + * Algorithm) header value or {@code null} if not present. + * + *

The JWE {@code enc} (encryption algorithm) Header Parameter identifies the content encryption algorithm + * used to perform authenticated encryption on the plaintext to produce the ciphertext and the JWE + * {@code Authentication Tag}.

+ * + *

Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an {@link AeadAlgorithm} to a {@link JwtBuilder} via one of its + * {@link JwtBuilder#encryptWith(SecretKey, AeadAlgorithm) encryptWith(SecretKey, AeadAlgorithm)} or + * {@link JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) encryptWith(Key, KeyAlgorithm, AeadAlgorithm)} + * methods. JJWT will then set this {@code enc} header value automatically to the {@code AeadAlgorithm}'s + * {@link AeadAlgorithm#getId() getId()} value during encryption.

+ * + * @return the JWE {@code enc} (Encryption Algorithm) header value or {@code null} if not present. This will + * always be {@code non-null} on validly-constructed JWE instances, but could be {@code null} during construction. + * @see JwtBuilder#encryptWith(SecretKey, AeadAlgorithm) + * @see JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) + */ + String getEncryptionAlgorithm(); + + /** + * Returns the {@code epk} (Ephemeral + * Public Key) header value created by the JWE originator for use with key agreement algorithms, or + * {@code null} if not present. + * + *

Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an ECDH-ES {@link KeyAlgorithm} to a {@link JwtBuilder} via its + * {@link JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) encryptWith(Key, KeyAlgorithm, AeadAlgorithm)} + * method. The ECDH-ES {@code KeyAlgorithm} implementation will then set this {@code epk} header value + * automatically when producing the encryption key.

+ * + * @return the {@code epk} (Ephemeral + * Public Key) header value created by the JWE originator for use with key agreement algorithms, or + * {@code null} if not present. + * @see Jwts#KEY + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + PublicJwk getEphemeralPublicKey(); + + /** + * Returns any information about the JWE producer for use with key agreement algorithms, or {@code null} if not + * present. + * + * @return any information about the JWE producer for use with key agreement algorithms, or {@code null} if not + * present. + * @see JWE apu (Agreement PartyUInfo) Header Parameter + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + byte[] getAgreementPartyUInfo(); + + /** + * Returns any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not + * present. + * + * @return any information about the JWE recipient for use with key agreement algorithms, or {@code null} if not + * present. + * @see JWE apv (Agreement PartyVInfo) Header Parameter + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + byte[] getAgreementPartyVInfo(); + + /** + * Returns the 96-bit "iv" + * (Initialization Vector) generated during key encryption, or {@code null} if not present. + * Set by AES GCM {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} implementations. + * + *

Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an AES GCM Wrap {@link KeyAlgorithm} to a {@link JwtBuilder} via its + * {@link JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) encryptWith(Key, KeyAlgorithm, AeadAlgorithm)} + * method. The AES GCM Wrap {@code KeyAlgorithm} implementation will then set this {@code iv} header value + * automatically when producing the encryption key.

+ * + * @return the 96-bit initialization vector generated during key encryption, or {@code null} if not present. + * @see StandardKeyAlgorithms#A128GCMKW + * @see StandardKeyAlgorithms#A192GCMKW + * @see StandardKeyAlgorithms#A256GCMKW + */ + byte[] getInitializationVector(); + + /** + * Returns the 128-bit "tag" + * (Authentication Tag) resulting from key encryption, or {@code null} if not present. + * + *

Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying an AES GCM Wrap {@link KeyAlgorithm} to a {@link JwtBuilder} via its + * {@link JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) encryptWith(Key, KeyAlgorithm, AeadAlgorithm)} + * method. The AES GCM Wrap {@code KeyAlgorithm} implementation will then set this {@code tag} header value + * automatically when producing the encryption key.

+ * + * @return the 128-bit authentication tag resulting from key encryption, or {@code null} if not present. + * @see StandardKeyAlgorithms#A128GCMKW + * @see StandardKeyAlgorithms#A192GCMKW + * @see StandardKeyAlgorithms#A256GCMKW + */ + byte[] getAuthenticationTag(); + + /** + * Returns the number of PBKDF2 iterations necessary to derive the key used during JWE encryption, or {@code null} + * if not present. Used with password-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm}s. + * + * @return the number of PBKDF2 iterations necessary to derive the key used during JWE encryption, or {@code null} + * if not present. + * @see JWE p2c (PBES2 Count) Header Parameter + * @see StandardKeyAlgorithms#PBES2_HS256_A128KW + * @see StandardKeyAlgorithms#PBES2_HS384_A192KW + * @see StandardKeyAlgorithms#PBES2_HS512_A256KW + */ + Integer getPbes2Count(); + + /** + * Returns the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption, or + * {@code null} if not present. + * + *

Note that there is no corresponding 'setter' method for this 'getter' because JJWT users set this value by + * supplying a password-based {@link KeyAlgorithm} to a {@link JwtBuilder} via its + * {@link JwtBuilder#encryptWith(Key, KeyAlgorithm, AeadAlgorithm) encryptWith(Key, KeyAlgorithm, AeadAlgorithm)} + * method. The password-based {@code KeyAlgorithm} implementation will then set this {@code p2s} header value + * automatically when producing the encryption key.

+ * + * @return the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption, or + * {@code null} if not present. + * @see JWE p2s (PBES2 Salt Input) Header Parameter + * @see StandardKeyAlgorithms#PBES2_HS256_A128KW + * @see StandardKeyAlgorithms#PBES2_HS384_A192KW + * @see StandardKeyAlgorithms#PBES2_HS512_A256KW + */ + byte[] getPbes2Salt(); +} diff --git a/api/src/main/java/io/jsonwebtoken/JweHeaderMutator.java b/api/src/main/java/io/jsonwebtoken/JweHeaderMutator.java new file mode 100644 index 00000000..07e3e775 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/JweHeaderMutator.java @@ -0,0 +1,146 @@ +/* + * 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; + +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.StandardKeyAlgorithms; + +/** + * Mutation (modifications) to a {@link JweHeader} instance. + * + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface JweHeaderMutator> extends ProtectedHeaderMutator { + +// /** +// * Sets the JWE {@code enc} (Encryption +// * Algorithm) header value. A {@code null} value will remove the property from the JSON map. +// * +// *

This should almost never be set by JJWT users directly - JJWT will always set this value to the value +// * returned by {@link AeadAlgorithm#getId()} when performing encryption, overwriting any potential previous +// * value.

+// * +// * @param enc the encryption algorithm identifier obtained from {@link AeadAlgorithm#getId()}. +// * @return this header for method chaining +// */ +// @SuppressWarnings("UnusedReturnValue") +// JweHeader setEncryptionAlgorithm(String enc); + + /** + * Sets any information about the JWE producer for use with key agreement algorithms. A {@code null} or empty value + * removes the property from the JSON map. + * + * @param info information about the JWE producer to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apu (Agreement PartyUInfo) Header Parameter + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + T setAgreementPartyUInfo(byte[] info); + + /** + * Sets any information about the JWE producer for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + *

If not {@code null}, this is a convenience method that calls the equivalent of the following:

+ *
+     * {@link #setAgreementPartyUInfo(byte[]) setAgreementPartyUInfo}(info.getBytes(StandardCharsets.UTF_8))
+ * + * @param info information about the JWE producer to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apu (Agreement PartyUInfo) Header Parameter + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + T setAgreementPartyUInfo(String info); + + /** + * Sets any information about the JWE recipient for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + * @param info information about the JWE recipient to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apv (Agreement PartyVInfo) Header Parameter + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + T setAgreementPartyVInfo(byte[] info); + + /** + * Sets any information about the JWE recipient for use with key agreement algorithms. A {@code null} value removes + * the property from the JSON map. + * + *

If not {@code null}, this is a convenience method that calls the equivalent of the following:

+ *
+     * {@link #setAgreementPartyVInfo(byte[]) setAgreementPartVUInfo}(info.getBytes(StandardCharsets.UTF_8))
+ * + * @param info information about the JWE recipient to use with key agreement algorithms. + * @return the header for method chaining. + * @see JWE apv (Agreement PartyVInfo) Header Parameter + * @see StandardKeyAlgorithms#ECDH_ES + * @see StandardKeyAlgorithms#ECDH_ES_A128KW + * @see StandardKeyAlgorithms#ECDH_ES_A192KW + * @see StandardKeyAlgorithms#ECDH_ES_A256KW + */ + T setAgreementPartyVInfo(String info); + + /** + * Sets the number of PBKDF2 iterations necessary to derive the key used during JWE encryption. If this value + * is not set when a password-based {@link KeyAlgorithm} is used, JJWT will automatically choose a suitable + * number of iterations based on + * OWASP PBKDF2 Iteration Recommendations. + * + *

Minimum Count

+ * + *

{@code IllegalArgumentException} will be thrown during encryption if a specified {@code count} is + * less than 1000 (one thousand), which is the + * minimum number recommended by the + * JWA specification. Anything less is susceptible to security attacks so the default PBKDF2 + * {@code KeyAlgorithm} implementations reject such values.

+ * + * @param count the number of PBKDF2 iterations necessary to derive the key used during JWE encryption, must be + * greater than or equal to 1000 (one thousand). + * @return the header for method chaining + * @see JWE p2c (PBES2 Count) Header Parameter + * @see StandardKeyAlgorithms#PBES2_HS256_A128KW + * @see StandardKeyAlgorithms#PBES2_HS384_A192KW + * @see StandardKeyAlgorithms#PBES2_HS512_A256KW + * @see OWASP PBKDF2 Iteration Recommendations + */ + T setPbes2Count(int count); + +// /** +// * Sets the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption. This should +// * almost never be used by JJWT users directly - it should instead be automatically generated and set within a +// * PBKDF2-based {@link io.jsonwebtoken.security.KeyAlgorithm KeyAlgorithm} implementation. +// * +// * @param salt the PBKDF2 {@code Salt Input} value necessary to derive the key used during JWE encryption. +// * @return the header for method chaining +// * @see JWE p2s (PBES2 Salt Input) Header Parameter +// * @see Jwts.KEY#PBES2_HS256_A128KW +// * @see Jwts.KEY#PBES2_HS384_A192KW +// * @see Jwts.KEY#PBES2_HS512_A256KW +// */ +// JweHeader setPbes2Salt(byte[] salt); + +} diff --git a/api/src/main/java/io/jsonwebtoken/Jws.java b/api/src/main/java/io/jsonwebtoken/Jws.java index 1be5fb39..8264386e 100644 --- a/api/src/main/java/io/jsonwebtoken/Jws.java +++ b/api/src/main/java/io/jsonwebtoken/Jws.java @@ -19,10 +19,14 @@ package io.jsonwebtoken; * An expanded (not compact/serialized) Signed JSON Web Token. * * @param the type of the JWS body contents, either a String or a {@link Claims} instance. - * * @since 0.1 */ -public interface Jws extends Jwt { +public interface Jws extends Jwt { - String getSignature(); + /** + * Returns the verified JWS signature as a Base64Url string. + * + * @return the verified JWS signature as a Base64Url string. + */ + String getSignature(); //TODO for 1.0: return a byte[] } diff --git a/api/src/main/java/io/jsonwebtoken/JwsHeader.java b/api/src/main/java/io/jsonwebtoken/JwsHeader.java index aaf08d86..2a0561cf 100644 --- a/api/src/main/java/io/jsonwebtoken/JwsHeader.java +++ b/api/src/main/java/io/jsonwebtoken/JwsHeader.java @@ -16,92 +16,54 @@ package io.jsonwebtoken; /** - * A JWS header. + * A JWS header. * - * @param header type * @since 0.1 */ -public interface JwsHeader> extends Header { - - /** JWS {@code Algorithm} header parameter name: "alg" */ - public static final String ALGORITHM = "alg"; - - /** JWS {@code JWT Set URL} header parameter name: "jku" */ - public static final String JWK_SET_URL = "jku"; - - /** JWS {@code JSON Web Key} header parameter name: "jwk" */ - public static final String JSON_WEB_KEY = "jwk"; - - /** JWS {@code Key ID} header parameter name: "kid" */ - public static final String KEY_ID = "kid"; - - /** JWS {@code X.509 URL} header parameter name: "x5u" */ - public static final String X509_URL = "x5u"; - - /** JWS {@code X.509 Certificate Chain} header parameter name: "x5c" */ - public static final String X509_CERT_CHAIN = "x5c"; - - /** JWS {@code X.509 Certificate SHA-1 Thumbprint} header parameter name: "x5t" */ - public static final String X509_CERT_SHA1_THUMBPRINT = "x5t"; - - /** JWS {@code X.509 Certificate SHA-256 Thumbprint} header parameter name: "x5t#S256" */ - public static final String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; - - /** JWS {@code Critical} header parameter name: "crit" */ - public static final String CRITICAL = "crit"; +public interface JwsHeader extends ProtectedHeader { /** - * Returns the JWS - * alg (algorithm) header value or {@code null} if not present. - * - *

The algorithm header parameter identifies the cryptographic algorithm used to secure the JWS. Consider - * using {@link io.jsonwebtoken.SignatureAlgorithm#forName(String) SignatureAlgorithm.forName} to convert this - * string value to a type-safe enum instance.

- * - * @return the JWS {@code alg} header value or {@code null} if not present. This will always be - * {@code non-null} on validly constructed JWS instances, but could be {@code null} during construction. + * JWS Algorithm Header name: the string literal alg */ - String getAlgorithm(); + String ALGORITHM = "alg"; /** - * Sets the JWT - * alg (Algorithm) header value. A {@code null} value will remove the property from the JSON map. - * - *

The algorithm header parameter identifies the cryptographic algorithm used to secure the JWS. Consider - * using a type-safe {@link io.jsonwebtoken.SignatureAlgorithm SignatureAlgorithm} instance and using its - * {@link io.jsonwebtoken.SignatureAlgorithm#getValue() value} as the argument to this method.

- * - * @param alg the JWS {@code alg} header value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. + * JWS JWK Set URL Header name: the string literal jku */ - T setAlgorithm(String alg); + String JWK_SET_URL = "jku"; /** - * Returns the JWS - * kid (Key ID) header value or {@code null} if not present. - * - *

The keyId header parameter is a hint indicating which key was used to secure the JWS. This parameter allows - * originators to explicitly signal a change of key to recipients. The structure of the keyId value is - * unspecified.

- * - *

When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

- * - * @return the JWS {@code kid} header value or {@code null} if not present. + * JWS JSON Web Key Header name: the string literal jwk */ - String getKeyId(); + String JSON_WEB_KEY = "jwk"; /** - * Sets the JWT - * kid (Key ID) header value. A {@code null} value will remove the property from the JSON map. - * - *

The keyId header parameter is a hint indicating which key was used to secure the JWS. This parameter allows - * originators to explicitly signal a change of key to recipients. The structure of the keyId value is - * unspecified.

- * - *

When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

- * - * @param kid the JWS {@code kid} header value or {@code null} to remove the property from the JSON map. - * @return the {@code Header} instance for method chaining. + * JWS Key ID Header name: the string literal kid */ - T setKeyId(String kid); + String KEY_ID = "kid"; + + /** + * JWS X.509 URL Header name: the string literal x5u + */ + String X509_URL = "x5u"; + + /** + * JWS X.509 Certificate Chain Header name: the string literal x5c + */ + String X509_CERT_CHAIN = "x5c"; + + /** + * JWS X.509 Certificate SHA-1 Thumbprint Header name: the string literal x5t + */ + String X509_CERT_SHA1_THUMBPRINT = "x5t"; + + /** + * JWS X.509 Certificate SHA-256 Thumbprint Header name: the string literal x5t#S256 + */ + String X509_CERT_SHA256_THUMBPRINT = "x5t#S256"; + + /** + * JWS Critical Header name: the string literal crit + */ + String CRITICAL = "crit"; } diff --git a/api/src/main/java/io/jsonwebtoken/Jwt.java b/api/src/main/java/io/jsonwebtoken/Jwt.java index 1de5c6e3..839b7b23 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwt.java +++ b/api/src/main/java/io/jsonwebtoken/Jwt.java @@ -18,11 +18,10 @@ package io.jsonwebtoken; /** * An expanded (not compact/serialized) JSON Web Token. * - * @param the type of the JWT body contents, either a String or a {@link Claims} instance. - * + * @param

the type of the JWT payload, either a byte array or a {@link Claims} instance. * @since 0.1 */ -public interface Jwt { +public interface Jwt, P> { /** * Returns the JWT {@link Header} or {@code null} if not present. @@ -32,9 +31,26 @@ public interface Jwt { H getHeader(); /** - * Returns the JWT body, either a {@code String} or a {@code Claims} instance. + * Returns the JWT payload, either a {@code byte[]} or a {@code Claims} instance. Use + * {@link #getPayload()} instead, as this method will be removed prior to the 1.0 release. * - * @return the JWT body, either a {@code String} or a {@code Claims} instance. + * @return the JWT payload, either a {@code byte[]} or a {@code Claims} instance. + * @deprecated since JJWT_RELEASE_VERSION because it has been renamed to {@link #getPayload()}. 'Payload' (not + * body) is what the JWT specifications call this property, so it has been renamed to reflect the correct JWT + * nomenclature/taxonomy. */ - B getBody(); + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + P getBody(); // TODO: remove for 1.0 + + /** + * Returns the JWT payload, either a {@code byte[]} or a {@code Claims} instance. If the payload is a byte + * array, and if the JWT creator set the (optional) {@link Header#getContentType() contentType} header + * value, the application may inspect the {@code contentType} value to determine how to convert the byte array to + * the final content type as desired. + * + * @return the JWT payload, either a {@code byte[]} or a {@code Claims} instance. + * @since JJWT_RELEASE_VERSION + */ + P getPayload(); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index b2a59740..3b81f284 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -19,21 +19,55 @@ import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.lang.Builder; +import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.StandardKeyAlgorithms; +import io.jsonwebtoken.security.StandardSecureDigestAlgorithms; +import io.jsonwebtoken.security.WeakKeyException; +import javax.crypto.SecretKey; import java.security.Key; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; import java.util.Date; import java.util.Map; /** - * A builder for constructing JWTs. + * A builder for constructing Unprotected JWTs, Signed JWTs (aka 'JWS's) and Encrypted JWTs (aka 'JWE's). * * @since 0.1 */ public interface JwtBuilder extends ClaimsMutator { - //replaces any existing header with the specified header. + /** + * Sets the JCA Provider to use during cryptographic signing or encryption operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during cryptographic signing or encryption operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setProvider(Provider provider); + + /** + * Sets the {@link SecureRandom} to use during cryptographic signing or encryption operations, or {@code null} if + * a default {@link SecureRandom} should be used. + * + * @param secureRandom the {@link SecureRandom} to use during cryptographic signing or encryption operations, or + * {@code null} if a default {@link SecureRandom} should be used. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setSecureRandom(SecureRandom secureRandom); /** * Sets (and replaces) any existing header with the specified header. If you do not want to replace the existing @@ -42,7 +76,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param header the header to set (and potentially replace any existing header). * @return the builder for method chaining. */ - JwtBuilder setHeader(Header header); + JwtBuilder setHeader(Header header); //replaces any existing header with the specified header. /** * Sets (and replaces) any existing header with the specified header. If you do not want to replace the existing @@ -51,7 +85,17 @@ public interface JwtBuilder extends ClaimsMutator { * @param header the header to set (and potentially replace any existing header). * @return the builder for method chaining. */ - JwtBuilder setHeader(Map header); + JwtBuilder setHeader(Map header); + + /** + * Sets (and replaces) any existing header with the header resulting from the specified builder's + * {@link Builder#build()} result. + * + * @param builder the builder to use to obtain the header + * @return the JwtBuilder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setHeader(Builder> builder); /** * Applies the specified name/value pairs to the header. If a header does not yet exist at the time this method @@ -60,9 +104,7 @@ public interface JwtBuilder extends ClaimsMutator { * @param params the header name/value pairs to append to the header. * @return the builder for method chaining. */ - JwtBuilder setHeaderParams(Map params); - - //sets the specified header parameter, overwriting any previous value under the same name. + JwtBuilder setHeaderParams(Map params); /** * Applies the specified name/value pair to the header. If a header does not yet exist at the time this method @@ -75,35 +117,97 @@ public interface JwtBuilder extends ClaimsMutator { JwtBuilder setHeaderParam(String name, Object value); /** - * Sets the JWT's payload to be a plaintext (non-JSON) string. If you want the JWT body to be JSON, use the - * {@link #setClaims(Claims)} or {@link #setClaims(java.util.Map)} methods instead. + * Sets the JWT payload to the string's UTF-8-encoded bytes. It is strongly recommended to also set the + * {@link Header#getContentType() contentType} header value so the JWT recipient may inspect that value to + * determine how to convert the byte array to the final data type as desired. In this case, consider using + * {@link #setContent(byte[], String)} instead. + * + *

This is a convenience method that is effectively the same as:

+ *
+     * {@link #setContent(byte[]) setPayload}(payload.getBytes(StandardCharsets.UTF_8));
+ * + *

If you want the JWT payload to be JSON, use the + * {@link #setClaims(Claims)} or {@link #setClaims(java.util.Map)} methods instead.

* *

The payload and claims properties are mutually exclusive - only one of the two may be used.

* - * @param payload the plaintext (non-JSON) string that will be the body of the JWT. + * @param payload the string used to set UTF-8-encoded bytes as the JWT payload. * @return the builder for method chaining. + * @see #setContent(byte[]) + * @see #setContent(byte[], String) + * @deprecated since JJWT_RELEASE VERSION in favor of {@link #setContent(byte[])} or {@link #setContent(byte[], String)} + * because both Claims and Content are technically 'payloads', so this method name is misleading. This method will + * be removed before the 1.0 release. */ + @Deprecated JwtBuilder setPayload(String payload); /** - * Sets the JWT payload to be a JSON Claims instance. If you do not want the JWT body to be JSON and instead want - * it to be a plaintext string, use the {@link #setPayload(String)} method instead. + * Sets the JWT payload to be the specified content byte array. + * + *

Content Type Recommendation

+ * + *

Unless you are confident that the JWT recipient will always know how to use + * the given byte array without additional metadata, it is strongly recommended to use the + * {@link #setContent(byte[], String)} method instead of this one. That method ensures that a JWT recipient + * can inspect the {@code cty} header to know how to handle the byte array without ambiguity.

+ * + *

Note that the content and claims properties are mutually exclusive - only one of the two may be used.

+ * + * @param content the content byte array to use as the JWT payload + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setContent(byte[] content); + + /** + * Convenience method that sets the JWT payload to be the specified content byte array and also sets the + * {@link Header#setContentType(String) contentType} header value to a compact {@code cty} 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. + * + *

Compact Media Type Identifier

+ * + *

As a convenience, this method will automatically trim any application/ prefix from the + * {@code cty} string if possible according to the + * JWT specification recommendations.

+ * + *

If for some reason you do not wish to adhere to the JWT specification recommendation, do not call this + * method - instead call {@link #setContent(byte[])} and {@link Header#setContentType(String)} independently.

+ * + *

If you want the JWT payload to be JSON claims, use the {@link #setClaims(Claims)} or + * {@link #setClaims(java.util.Map)} methods instead.

+ * + *

Note that the content and claims properties are mutually exclusive - only one of the two may be used.

+ * + * @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 payload} or {@code cty} are null or empty. + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder setContent(byte[] content, String cty) throws IllegalArgumentException; + + /** + * Sets the JWT payload to be a JSON Claims instance. If you do not want the JWT payload to be JSON claims and + * instead want it to be a byte array representing any type of content, use the {@link #setContent(byte[])} + * method instead. * *

The payload and claims properties are mutually exclusive - only one of the two may be used.

* - * @param claims the JWT claims to be set as the JWT body. + * @param claims the JWT claims to be set as the JWT payload. * @return the builder for method chaining. */ JwtBuilder setClaims(Claims claims); /** * Sets the JWT payload to be a JSON Claims instance populated by the specified name/value pairs. If you do not - * want the JWT body to be JSON and instead want it to be a plaintext string, use the {@link #setPayload(String)} - * method instead. + * want the JWT payload to be JSON claims and instead want it to be a byte array for any content, use the + * {@link #setContent(byte[])} or {@link #setContent(byte[], String)} methods instead. * - *

The payload* and claims* properties are mutually exclusive - only one of the two may be used.

+ *

The payload and claims properties are mutually exclusive - only one of the two may be used.

* - * @param claims the JWT claims to be set as the JWT body. + * @param claims the JWT Claims to be set as the JWT payload. * @return the builder for method chaining. */ JwtBuilder setClaims(Map claims); @@ -114,17 +218,17 @@ public interface JwtBuilder extends ClaimsMutator { * *

The payload and claims properties are mutually exclusive - only one of the two may be used.

* - * @param claims the JWT claims to be added to the JWT body. + * @param claims the JWT Claims to be added to the JWT payload. * @return the builder for method chaining. * @since 0.8 */ - JwtBuilder addClaims(Map claims); + JwtBuilder addClaims(Map claims); /** * Sets the JWT Claims * iss (issuer) value. A {@code null} value will remove the property from the Claims. * - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setIssuer(String) issuer} field with the specified value. This allows you to write * code like this:

* @@ -151,7 +255,7 @@ public interface JwtBuilder extends ClaimsMutator { * Sets the JWT Claims * sub (subject) value. A {@code null} value will remove the property from the Claims. * - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setSubject(String) subject} field with the specified value. This allows you to write * code like this:

* @@ -178,7 +282,7 @@ public interface JwtBuilder extends ClaimsMutator { * Sets the JWT Claims * aud (audience) value. A {@code null} value will remove the property from the Claims. * - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setAudience(String) audience} field with the specified value. This allows you to write * code like this:

* @@ -207,7 +311,7 @@ public interface JwtBuilder extends ClaimsMutator { * *

A JWT obtained after this timestamp should not be used.

* - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setExpiration(java.util.Date) expiration} field with the specified value. This allows * you to write code like this:

* @@ -236,7 +340,7 @@ public interface JwtBuilder extends ClaimsMutator { * *

A JWT obtained before this timestamp should not be used.

* - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setNotBefore(java.util.Date) notBefore} field with the specified value. This allows * you to write code like this:

* @@ -265,7 +369,7 @@ public interface JwtBuilder extends ClaimsMutator { * *

The value is the timestamp when the JWT was created.

* - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setIssuedAt(java.util.Date) issuedAt} field with the specified value. This allows * you to write code like this:

* @@ -296,7 +400,7 @@ public interface JwtBuilder extends ClaimsMutator { * manner that ensures that there is a negligible probability that the same value will be accidentally * assigned to a different data object. The ID can be used to prevent the JWT from being replayed.

* - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set * the Claims {@link Claims#setId(String) id} field with the specified value. This allows * you to write code like this:

* @@ -322,7 +426,7 @@ public interface JwtBuilder extends ClaimsMutator { /** * Sets a custom JWT Claims parameter value. A {@code null} value will remove the property from the Claims. * - *

This is a convenience method. It will first ensure a Claims instance exists as the JWT body and then set the + *

This is a convenience method. It will first ensure a Claims instance exists as the JWT payload and then set the * named property on the Claims instance using the Claims {@link Claims#put(Object, Object) put} method. This allows * you to write code like this:

* @@ -345,20 +449,144 @@ public interface JwtBuilder extends ClaimsMutator { JwtBuilder claim(String name, Object value); /** - * Signs the constructed JWT with the specified key using the key's - * {@link SignatureAlgorithm#forSigningKey(Key) recommended signature algorithm}, producing a JWS. If the - * recommended signature algorithm isn't sufficient for your needs, consider using - * {@link #signWith(Key, SignatureAlgorithm)} instead. + * Signs the constructed JWT with the specified key using the key's recommended signature algorithm + * as defined below, producing a JWS. If the recommended signature algorithm isn't sufficient for your needs, + * consider using {@link #signWith(Key, SecureDigestAlgorithm)} instead. * *

If you are looking to invoke this method with a byte array that you are confident may be used for HMAC-SHA * algorithms, consider using {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to * convert the byte array into a valid {@code Key}.

* + *

Recommended Signature Algorithm

+ * + *

The recommended signature algorithm used with a given key is chosen based on the following:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Key Recommended Signature Algorithm
If the Key is a:And:With a key size of:The SignatureAlgorithm used will be:
{@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA256")1256 <= size <= 383 2{@link StandardSecureDigestAlgorithms#HS256 HS256}
{@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA384")1384 <= size <= 511{@link StandardSecureDigestAlgorithms#HS384 HS384}
{@link SecretKey}{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA512")1512 <= size{@link StandardSecureDigestAlgorithms#HS512 HS512}
{@link ECKey}instanceof {@link PrivateKey}256 <= size <= 383 3{@link StandardSecureDigestAlgorithms#ES256 ES256}
{@link ECKey}instanceof {@link PrivateKey}384 <= size <= 520 4{@link StandardSecureDigestAlgorithms#ES384 ES384}
{@link ECKey}instanceof {@link PrivateKey}521 <= size 4{@link StandardSecureDigestAlgorithms#ES512 ES512}
{@link RSAKey}instanceof {@link PrivateKey}2048 <= size <= 3071 5,6{@link StandardSecureDigestAlgorithms#RS256 RS256}
{@link RSAKey}instanceof {@link PrivateKey}3072 <= size <= 4095 6{@link StandardSecureDigestAlgorithms#RS384 RS384}
{@link RSAKey}instanceof {@link PrivateKey}4096 <= size 5{@link StandardSecureDigestAlgorithms#RS512 RS512}
EdECKey7instanceof {@link PrivateKey}256{@link StandardSecureDigestAlgorithms#Ed25519 Ed25519}
EdECKey7instanceof {@link PrivateKey}456{@link StandardSecureDigestAlgorithms#Ed448 Ed448}
+ *

Notes:

+ *
    + *
  1. {@code SecretKey} instances must have an {@link Key#getAlgorithm() algorithm} name equal + * to {@code HmacSHA256}, {@code HmacSHA384} or {@code HmacSHA512}. If not, the key bytes might not be + * suitable for HMAC signatures will be rejected with a {@link InvalidKeyException}.
  2. + *
  3. The JWT JWA Specification (RFC 7518, + * Section 3.2) mandates that HMAC-SHA-* signing keys MUST be 256 bits or greater. + * {@code SecretKey}s with key lengths less than 256 bits will be rejected with an + * {@link WeakKeyException}.
  4. + *
  5. The JWT JWA Specification (RFC 7518, + * Section 3.4) mandates that ECDSA signing key lengths MUST be 256 bits or greater. + * {@code ECKey}s with key lengths less than 256 bits will be rejected with a + * {@link WeakKeyException}.
  6. + *
  7. The ECDSA {@code P-521} curve does indeed use keys of 521 bits, not 512 as might be expected. ECDSA + * keys of 384 < size <= 520 are suitable for ES384, while ES512 requires keys >= 521 bits. The '512' part of the + * ES512 name reflects the usage of the SHA-512 algorithm, not the ECDSA key length. ES512 with ECDSA keys less + * than 521 bits will be rejected with a {@link WeakKeyException}.
  8. + *
  9. The JWT JWA Specification (RFC 7518, + * Section 3.3) mandates that RSA signing key lengths MUST be 2048 bits or greater. + * {@code RSAKey}s with key lengths less than 2048 bits will be rejected with a + * {@link WeakKeyException}.
  10. + *
  11. Technically any RSA key of length >= 2048 bits may be used with the + * {@link StandardSecureDigestAlgorithms#RS256 RS256}, {@link StandardSecureDigestAlgorithms#RS384 RS384}, and + * {@link StandardSecureDigestAlgorithms#RS512 RS512} algorithms, so we assume an RSA signature algorithm based on the key + * length to parallel similar decisions in the JWT specification for HMAC and ECDSA signature algorithms. + * This is not required - just a convenience.
  12. + *
  13. EdECKeys + * require JDK >= 15 or BouncyCastle in the runtime classpath.
  14. + *
+ * + *

This implementation does not use the {@link StandardSecureDigestAlgorithms#PS256 PS256}, + * {@link StandardSecureDigestAlgorithms#PS384 PS384}, or {@link StandardSecureDigestAlgorithms#PS512 PS512} RSA variants for any + * specified {@link RSAKey} because the the {@link StandardSecureDigestAlgorithms#RS256 RS256}, + * {@link StandardSecureDigestAlgorithms#RS384 RS384}, and {@link StandardSecureDigestAlgorithms#RS512 RS512} algorithms are + * available in the JDK by default while the {@code PS}* variants require either JDK 11 or an additional JCA + * Provider (like BouncyCastle). If you wish to use a {@code PS}* variant with your key, use the + * {@link #signWith(Key, SecureDigestAlgorithm)} method instead.

+ * + *

Finally, this method will throw an {@link InvalidKeyException} for any key that does not match the + * heuristics and requirements documented above, since that inevitably means the Key is either insufficient, + * unsupported, or explicitly disallowed by the JWT specification.

+ * * @param key the key to use for signing * @return the builder instance for method chaining. - * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as - * described by {@link SignatureAlgorithm#forSigningKey(Key)}. - * @see #signWith(Key, SignatureAlgorithm) + * @throws InvalidKeyException if the Key is insufficient, unsupported, or explicitly disallowed by the JWT + * specification as described above in recommended signature algorithms. + * @see Jwts#SIG + * @see #signWith(Key, SecureDigestAlgorithm) * @since 0.10.0 */ JwtBuilder signWith(Key key) throws InvalidKeyException; @@ -369,17 +597,19 @@ public interface JwtBuilder extends ClaimsMutator { *

Deprecation Notice: Deprecated as of 0.10.0

* *

Use {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to - * obtain the {@code Key} and then invoke {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)}.

+ * obtain the {@code Key} and then invoke {@link #signWith(Key)} or + * {@link #signWith(Key, SecureDigestAlgorithm)}.

* *

This method will be removed in the 1.0 release.

* * @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS. * @param secretKey the algorithm-specific signing key to use to digitally sign the JWT. * @return the builder for method chaining. - * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as - * described by {@link SignatureAlgorithm#forSigningKey(Key)}. + * @throws InvalidKeyException if the Key is insufficient for the specified algorithm or explicitly disallowed by + * the JWT specification. * @deprecated as of 0.10.0: use {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to - * obtain the {@code Key} and then invoke {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)}. + * obtain the {@code Key} and then invoke {@link #signWith(Key)} or + * {@link #signWith(Key, SecureDigestAlgorithm)}. * This method will be removed in the 1.0 release. */ @Deprecated @@ -403,7 +633,7 @@ public interface JwtBuilder extends ClaimsMutator { *

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

* *

However, a non-trivial number of JJWT users were confused by the method signature and attempted to - * use raw password strings as the key argument - for example {@code signWith(HS256, myPassword)} - which is + * use raw password strings as the key argument - for example {@code with(HS256, myPassword)} - which is * almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.

* *

See this @@ -415,7 +645,7 @@ public interface JwtBuilder extends ClaimsMutator { *


      * byte[] keyBytes = {@link Decoders Decoders}.{@link Decoders#BASE64 BASE64}.{@link Decoder#decode(Object) decode(base64EncodedSecretKey)};
      * Key key = {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(keyBytes)};
-     * jwtBuilder.signWith(key); //or {@link #signWith(Key, SignatureAlgorithm)}
+     * jwtBuilder.with(key); //or {@link #signWith(Key, SignatureAlgorithm)}
      * 
* *

This method will be removed in the 1.0 release.

@@ -445,14 +675,21 @@ public interface JwtBuilder extends ClaimsMutator { * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for * the specified algorithm. * @see #signWith(Key) - * @deprecated since 0.10.0: use {@link #signWith(Key, SignatureAlgorithm)} instead. This method will be removed - * in the 1.0 release. + * @deprecated since 0.10.0. Use {@link #signWith(Key, SecureDigestAlgorithm)} instead. + * This method will be removed before the 1.0 release. */ @Deprecated JwtBuilder signWith(SignatureAlgorithm alg, Key key) throws InvalidKeyException; /** - * Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS. + *

Deprecation Notice

+ * + *

This has been deprecated since JJWT_RELEASE_VERSION. Use + * {@link #signWith(Key, SecureDigestAlgorithm)} instead. Standard JWA algorithms + * are represented as instances of this new interface in the {@link Jwts#SIG} + * algorithm registry.

+ * + *

Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS.

* *

It is typically recommended to call the {@link #signWith(Key)} instead for simplicity. * However, this method can be useful if the recommended algorithm heuristics do not meet your needs or if @@ -465,11 +702,97 @@ public interface JwtBuilder extends ClaimsMutator { * the specified algorithm. * @see #signWith(Key) * @since 0.10.0 + * @deprecated since JJWT_RELEASE_VERSION to use the more flexible {@link #signWith(Key, SecureDigestAlgorithm)}. */ + @Deprecated JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException; /** - * Compresses the JWT body using the specified {@link CompressionCodec}. + * Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS. + * + *

The {@link Jwts#SIG} registry makes available all standard signature + * algorithms defined in the JWA specification.

+ * + *

It is typically recommended to call the {@link #signWith(Key)} instead for simplicity. + * However, this method can be useful if the recommended algorithm heuristics do not meet your needs or if + * you want explicit control over the signature algorithm used with the specified key.

+ * + * @param key the signing key to use to digitally sign the JWT. + * @param The type of key accepted by the {@code SignatureAlgorithm}. + * @param alg the JWS algorithm to use with the key to digitally sign the JWT, thereby producing a JWS. + * @return the builder for method chaining. + * @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for + * the specified algorithm. + * @see #signWith(Key) + * @see Jwts#SIG + * @since JJWT_RELEASE_VERSION + */ + JwtBuilder signWith(K key, io.jsonwebtoken.security.SecureDigestAlgorithm alg) throws InvalidKeyException; + + /** + * Encrypts the constructed JWT with the specified symmetric {@code key} using the provided {@code enc}ryption + * algorithm, producing a JWE. Because it is a symmetric key, the JWE recipient + * must also have access to the same key to decrypt. + * + *

This method is a convenience method that delegates to + * {@link #encryptWith(Key, KeyAlgorithm, AeadAlgorithm) encryptWith(Key, KeyAlgorithm, AeadAlgorithm)} + * based on the {@code key} argument:

+ *
    + *
  • If the provided {@code key} is a {@link Password Password} instance, + * the {@code KeyAlgorithm} used will be one of the three JWA-standard password-based key algorithms + * ({@link StandardKeyAlgorithms#PBES2_HS256_A128KW PBES2_HS256_A128KW}, + * {@link StandardKeyAlgorithms#PBES2_HS384_A192KW PBES2_HS384_A192KW}, or + * {@link StandardKeyAlgorithms#PBES2_HS512_A256KW PBES2_HS512_A256KW}) as determined by the {@code enc} algorithm's + * {@link AeadAlgorithm#getKeyBitLength() key length} requirement.
  • + *
  • If the {@code key} is otherwise a standard {@code SecretKey}, the {@code KeyAlgorithm} will be + * {@link StandardKeyAlgorithms#DIRECT}, indicating that {@code key} should be used directly with the + * {@code enc} algorithm. In this case, the {@code key} argument MUST be of sufficient strength to + * use with the specified {@code enc} algorithm, otherwise an exception will be thrown during encryption. If + * desired, secure-random keys suitable for an {@link AeadAlgorithm} may be generated using the algorithm's + * {@link AeadAlgorithm#keyBuilder() keyBuilder}.
  • + *
+ * + * @param key the symmetric encryption key to use with the {@code enc} algorithm. + * @param enc the {@link AeadAlgorithm} algorithm used to encrypt the JWE, usually one of the JWA-standard + * algorithms accessible via {@link Jwts#ENC}. + * @return the JWE builder for method chaining. + * @see Jwts#ENC + */ + JwtBuilder encryptWith(SecretKey key, AeadAlgorithm enc); + + /** + * Encrypts the constructed JWT using the specified {@code enc} algorithm with the symmetric key produced by the + * {@code keyAlg} when invoked with the given {@code key}, producing a JWE. + * + *

This behavior can be illustrated by the following pseudocode, a rough example of what happens during + * {@link #compact() compact}ion:

+ *
+     *     SecretKey encryptionKey = keyAlg.getEncryptionKey(key);           // (1)
+     *     byte[] jweCiphertext = enc.encrypt(payloadBytes, encryptionKey);  // (2)
+ *
    + *
  1. The {@code keyAlg} argument is first invoked with the provided {@code key} argument, resulting in a + * {@link SecretKey}.
  2. + *
  3. This {@code SecretKey} result is used to call the provided {@code enc} encryption algorithm argument, + * resulting in the final JWE ciphertext.
  4. + *
+ * + *

Most application developers will reference one of the JWA + * {@link Jwts#KEY standard key algorithms} and {@link Jwts#ENC standard encryption algorithms} + * when invoking this method, but custom implementations are also supported.

+ * + * @param the type of key that must be used with the specified {@code keyAlg} instance. + * @param key the key used to invoke the provided {@code keyAlg} instance. + * @param keyAlg the key management algorithm that will produce the symmetric {@code SecretKey} to use with the + * {@code enc} algorithm + * @param enc the {@link AeadAlgorithm} algorithm used to encrypt the JWE + * @return the JWE builder for method chaining. + * @see Jwts#ENC + * @see Jwts#KEY + */ + JwtBuilder encryptWith(K key, KeyAlgorithm keyAlg, AeadAlgorithm enc); + + /** + * Compresses the JWT payload using the specified {@link CompressionCodec}. * *

If your compact JWTs are large, and you want to reduce their total size during network transmission, this * can be useful. For example, when embedding JWTs in URLs, some browsers may not support URLs longer than a @@ -477,7 +800,7 @@ public interface JwtBuilder extends ClaimsMutator { * *

Compatibility Warning

* - *

The JWT family of specifications defines compression only for JWE (Json Web Encryption) + *

The JWT family of specifications defines compression only for JWE (JSON Web Encryption) * tokens. Even so, JJWT will also support compression for JWS tokens as well if you choose to use it. * However, be aware that if you use compression when creating a JWS token, other libraries may not be able to * parse that JWS token. When using compression for JWS tokens, be sure that all parties accessing the @@ -507,7 +830,7 @@ public interface JwtBuilder extends ClaimsMutator { /** * Performs object-to-JSON serialization with the specified Serializer. This is used by the builder to convert - * JWT/JWS/JWT 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. * *

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 diff --git a/api/src/main/java/io/jsonwebtoken/JwtException.java b/api/src/main/java/io/jsonwebtoken/JwtException.java index c25aa223..e3990dab 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtException.java +++ b/api/src/main/java/io/jsonwebtoken/JwtException.java @@ -22,10 +22,21 @@ package io.jsonwebtoken; */ public class JwtException extends RuntimeException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public JwtException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public JwtException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandler.java b/api/src/main/java/io/jsonwebtoken/JwtHandler.java index 0e23f833..a9778607 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandler.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandler.java @@ -17,7 +17,7 @@ package io.jsonwebtoken; /** * A JwtHandler is invoked by a {@link io.jsonwebtoken.JwtParser JwtParser} after parsing a JWT to indicate the exact - * type of JWT or JWS parsed. + * type of JWT, JWS or JWE parsed. * * @param the type of object to return to the parser caller after handling the parsed JWT. * @since 0.2 @@ -26,37 +26,42 @@ public interface JwtHandler { /** * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is - * a plaintext JWT. A plaintext JWT has a String (non-JSON) body payload and it is not cryptographically signed. + * an Unprotected content JWT. An Unprotected content JWT has a byte array payload that is not + * cryptographically signed or encrypted. If the JWT creator set the (optional) + * {@link Header#getContentType() contentType} header value, the application may inspect that value to determine + * how to convert the byte array to the final content type as desired. * - * @param jwt the parsed plaintext JWT + * @param jwt the parsed Unprotected content JWT * @return any object to be used after inspecting the JWT, or {@code null} if no return value is necessary. */ - T onPlaintextJwt(Jwt jwt); + T onContentJwt(Jwt jwt); /** * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is - * a Claims JWT. A Claims JWT has a {@link Claims} body and it is not cryptographically signed. + * a Claims JWT. A Claims JWT has a {@link Claims} payload that is not cryptographically signed or encrypted. * * @param jwt the parsed claims JWT * @return any object to be used after inspecting the JWT, or {@code null} if no return value is necessary. */ - T onClaimsJwt(Jwt jwt); + T onClaimsJwt(Jwt jwt); /** * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is - * a plaintext JWS. A plaintext JWS is a JWT with a String (non-JSON) body (payload) that has been - * cryptographically signed. + * a content JWS. A content JWS is a JWT with a byte array payload that has been cryptographically signed. + * If the JWT creator set the (optional) {@link Header#getContentType() contentType} header value, the + * application may inspect that value to determine how to convert the byte array to the final content type + * as desired. * *

This method will only be invoked if the cryptographic signature can be successfully verified.

* - * @param jws the parsed plaintext JWS + * @param jws the parsed content JWS * @return any object to be used after inspecting the JWS, or {@code null} if no return value is necessary. */ - T onPlaintextJws(Jws jws); + T onContentJws(Jws jws); /** * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is - * a valid Claims JWS. A Claims JWS is a JWT with a {@link Claims} body that has been cryptographically signed. + * a valid Claims JWS. A Claims JWS is a JWT with a {@link Claims} payload that has been cryptographically signed. * *

This method will only be invoked if the cryptographic signature can be successfully verified.

* @@ -65,4 +70,30 @@ public interface JwtHandler { */ T onClaimsJws(Jws jws); + /** + * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is + * a content JWE. A content JWE is a JWE with a byte array payload that has been encrypted. If the JWT creator set + * the (optional) {@link Header#getContentType() contentType} header value, the application may inspect that + * value to determine how to convert the byte array to the final content type as desired. + * + *

This method will only be invoked if the content JWE can be successfully decrypted.

+ * + * @param jwe the parsed content jwe + * @return any object to be used after inspecting the JWE, or {@code null} if no return value is necessary. + * @since JJWT_RELEASE_VERSION + */ + T onContentJwe(Jwe jwe); + + /** + * This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} determines that the parsed JWT is + * a valid Claims JWE. A Claims JWE is a JWT with a {@link Claims} payload that has been encrypted. + * + *

This method will only be invoked if the Claims JWE can be successfully decrypted.

+ * + * @param jwe the parsed claims jwe + * @return any object to be used after inspecting the JWE, or {@code null} if no return value is necessary. + * @since JJWT_RELEASE_VERSION + */ + T onClaimsJwe(Jwe jwe); + } diff --git a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java index 74948837..d0677d61 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/JwtHandlerAdapter.java @@ -21,32 +21,42 @@ package io.jsonwebtoken; * known/expected for a particular use case. * *

All of the methods in this implementation throw exceptions: overridden methods represent - * scenarios expected by calling code in known situations. It would be unexpected to receive a JWS or JWT that did + * scenarios expected by calling code in known situations. It would be unexpected to receive a JWT that did * not match parsing expectations, so all non-overridden methods throw exceptions to indicate that the JWT * input was unexpected.

* * @param the type of object to return to the parser caller after handling the parsed JWT. * @since 0.2 */ -public class JwtHandlerAdapter implements JwtHandler { +public abstract class JwtHandlerAdapter implements JwtHandler { @Override - public T onPlaintextJwt(Jwt jwt) { - throw new UnsupportedJwtException("Unsigned plaintext JWTs are not supported."); + public T onContentJwt(Jwt jwt) { + throw new UnsupportedJwtException("Unprotected content JWTs are not supported."); } @Override - public T onClaimsJwt(Jwt jwt) { - throw new UnsupportedJwtException("Unsigned Claims JWTs are not supported."); + public T onClaimsJwt(Jwt jwt) { + throw new UnsupportedJwtException("Unprotected Claims JWTs are not supported."); } @Override - public T onPlaintextJws(Jws jws) { - throw new UnsupportedJwtException("Signed plaintext JWSs are not supported."); + public T onContentJws(Jws jws) { + throw new UnsupportedJwtException("Signed content JWTs are not supported."); } @Override public T onClaimsJws(Jws jws) { - throw new UnsupportedJwtException("Signed Claims JWSs are not supported."); + throw new UnsupportedJwtException("Signed Claims JWTs are not supported."); + } + + @Override + public T onContentJwe(Jwe jwe) { + throw new UnsupportedJwtException("Encrypted content JWTs are not supported."); + } + + @Override + public T onClaimsJwe(Jwe jwe) { + throw new UnsupportedJwtException("Encrypted Claims JWTs are not supported."); } } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index 04152253..995fef2d 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -17,6 +17,7 @@ package io.jsonwebtoken; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SignatureException; import java.security.Key; @@ -28,9 +29,17 @@ import java.util.Map; * * @since 0.1 */ +@SuppressWarnings("DeprecatedIsStillUsed") public interface JwtParser { - public static final char SEPARATOR_CHAR = '.'; + /** + * Deprecated - this was an implementation detail accidentally added to the public interface. This + * will be removed in a future release. + * + * @deprecated since JJWT_RELEASE_VERSION, to be removed in a future relase. + */ + @Deprecated + char SEPARATOR_CHAR = '.'; /** * Ensures that the specified {@code jti} exists in the parsed JWT. If missing or if the parsed @@ -213,7 +222,7 @@ public interface JwtParser { * @param key the algorithm-specific signature verification key used to validate any discovered JWS digital * signature. * @return the parser for method chaining. - * @deprecated see {@link JwtParserBuilder#setSigningKey(byte[])}. + * @deprecated in favor of {@link JwtParserBuilder#verifyWith(Key)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

NOTE: this method will be removed before version 1.0 @@ -259,7 +268,7 @@ public interface JwtParser { * @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate * any discovered JWS digital signature. * @return the parser for method chaining. - * @deprecated see {@link JwtParserBuilder#setSigningKey(String)}. + * @deprecated in favor of {@link JwtParserBuilder#verifyWith(Key)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

NOTE: this method will be removed before version 1.0 @@ -279,7 +288,7 @@ public interface JwtParser { * @param key the algorithm-specific signature verification key to use to validate any discovered JWS digital * signature. * @return the parser for method chaining. - * @deprecated see {@link JwtParserBuilder#setSigningKey(Key)}. + * @deprecated in favor of {@link JwtParserBuilder#verifyWith(Key)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

NOTE: this method will be removed before version 1.0 @@ -292,7 +301,7 @@ public interface JwtParser { * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used. * *

Specifying a {@code SigningKeyResolver} is necessary when the signing key is not already known before parsing - * the JWT and the JWT header or payload (plaintext body or Claims) must be inspected first to determine how to + * the JWT and the JWT header or payload (content byte array or Claims) must be inspected first to determine how to * look up the signing key. Once returned by the resolver, the JwtParser will then verify the JWS signature with the * returned key. For example:

* @@ -314,22 +323,24 @@ public interface JwtParser { * @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @return the parser for method chaining. * @since 0.4 - * @deprecated see {@link JwtParserBuilder#setSigningKeyResolver(SigningKeyResolver)}. + * @deprecated in favor of {@link JwtParserBuilder#setKeyLocator(Locator)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

NOTE: this method will be removed before version 1.0 */ + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated + // TODO: remove for 1.0 JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); /** * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to - * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. + * decompress the JWT payload. If the parsed JWT is not compressed, this resolver is not used. * - *

NOTE: Compression is not defined by the JWT Specification, and it is not expected that other libraries - * (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. This method is only - * useful if the compact JWT was compressed with JJWT >= 0.6.0 or another library that you know implements - * the same behavior.

+ *

NOTE: Compression is not defined by the JWS Specification - only the JWE Specification - and it is + * not expected that other libraries (including JJWT versions < 0.6.0) are able to consume a compressed JWS + * payload correctly. This method is only useful if the compact JWT was compressed with JJWT >= 0.6.0 or another + * library that you know implements the same behavior.

* *

Default Support

* @@ -341,10 +352,10 @@ public interface JwtParser { * your own {@link CompressionCodecResolver} and specify that via this method and also when * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} JWTs.

* - * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. + * @param compressionCodecResolver the compression codec resolver used to decompress the JWT payload. * @return the parser for method chaining. * @since 0.6.0 - * @deprecated see {@link JwtParserBuilder#setCompressionCodecResolver(CompressionCodecResolver)}. + * @deprecated in favor of {@link JwtParserBuilder#setCompressionCodecLocator(Locator)}. * To construct a JwtParser use the corresponding builder via {@link Jwts#parserBuilder()}. This will construct an * immutable JwtParser. *

NOTE: this method will be removed before version 1.0 @@ -397,42 +408,45 @@ public interface JwtParser { *

Note that if you are reasonably sure that the token is signed, it is more efficient to attempt to * parse the token (and catching exceptions if necessary) instead of calling this method first before parsing.

* - * @param jwt the compact serialized JWT to check + * @param compact the compact serialized JWT to check * @return {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false} * otherwise. */ - boolean isSigned(String jwt); + boolean isSigned(String compact); /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and - * returns the resulting JWT or JWS instance. + * returns the resulting JWT, JWS, or JWE instance. * - *

This method returns a JWT or JWS based on the parsed string. Because it may be cumbersome to determine if it - * is a JWT or JWS, or if the body/payload is a Claims or String with {@code instanceof} checks, the - * {@link #parse(String, JwtHandler) parse(String,JwtHandler)} method allows for a type-safe callback approach that - * may help reduce code or instanceof checks.

+ *

This method returns a JWT, JWS, or JWE based on the parsed string. Because it may be cumbersome to + * determine if it is a JWT, JWS or JWE, or if the payload is a Claims or byte array with {@code instanceof} checks, + * the {@link #parse(String, JwtHandler) parse(String,JwtHandler)} method allows for a type-safe callback approach + * that may help reduce code or instanceof checks.

* * @param jwt the compact serialized JWT to parse * @return the specified compact serialized JWT string based on the builder's current configuration state. * @throws MalformedJwtException if the specified JWT was incorrectly constructed (and therefore invalid). - * Invalid - * JWTs should not be trusted and should be discarded. + * Invalid JWTs should not be trusted and should be discarded. * @throws SignatureException if a JWS signature was discovered, but could not be verified. JWTs that fail * signature validation should not be trusted and should be discarded. + * @throws SecurityException if the specified JWT string is a JWE and decryption fails * @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time * before the time this method is invoked. * @throws IllegalArgumentException if the specified string is {@code null} or empty or only whitespace. * @see #parse(String, JwtHandler) - * @see #parsePlaintextJwt(String) + * @see #parseContentJwt(String) * @see #parseClaimsJwt(String) - * @see #parsePlaintextJws(String) + * @see #parseContentJws(String) * @see #parseClaimsJws(String) + * @see #parseContentJwe(String) + * @see #parseClaimsJwe(String) */ - Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, + SecurityException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and - * invokes the specified {@code handler} with the resulting JWT or JWS instance. + * invokes the specified {@code handler} with the resulting JWT, JWS, or JWE instance. * *

If you are confident of the format of the JWT before parsing, you can create an anonymous subclass using the * {@link io.jsonwebtoken.JwtHandlerAdapter JwtHandlerAdapter} and override only the methods you know are relevant @@ -453,145 +467,220 @@ public interface JwtParser { * following convenience methods instead of this one:

* *
    - *
  • {@link #parsePlaintextJwt(String)}
  • + *
  • {@link #parseContentJwt(String)}
  • *
  • {@link #parseClaimsJwt(String)}
  • - *
  • {@link #parsePlaintextJws(String)}
  • + *
  • {@link #parseContentJws(String)}
  • *
  • {@link #parseClaimsJws(String)}
  • + *
  • {@link #parseContentJwe(String)}
  • + *
  • {@link #parseClaimsJwe(String)}
  • *
* - * @param jwt the compact serialized JWT to parse + * @param jwt the compact serialized JWT to parse * @param handler the handler to invoke when encountering a specific type of JWT - * @param the type of object returned from the {@code handler} + * @param the type of object returned from the {@code handler} * @return the result returned by the {@code JwtHandler} * @throws MalformedJwtException if the specified JWT was incorrectly constructed (and therefore invalid). * Invalid JWTs should not be trusted and should be discarded. * @throws SignatureException if a JWS signature was discovered, but could not be verified. JWTs that fail * signature validation should not be trusted and should be discarded. + * @throws SecurityException if the specified JWT string is a JWE and decryption fails * @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time * before the time this method is invoked. * @throws IllegalArgumentException if the specified string is {@code null} or empty or only whitespace, or if the * {@code handler} is {@code null}. - * @see #parsePlaintextJwt(String) + * @see #parseContentJwt(String) * @see #parseClaimsJwt(String) - * @see #parsePlaintextJws(String) + * @see #parseContentJws(String) * @see #parseClaimsJws(String) + * @see #parseContentJwe(String) + * @see #parseClaimsJwe(String) * @see #parse(String) * @since 0.2 */ - T parse(String jwt, JwtHandler handler) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + T parse(String jwt, JwtHandler handler) throws ExpiredJwtException, UnsupportedJwtException, + MalformedJwtException, SignatureException, SecurityException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and - * returns the resulting unsigned plaintext JWT instance. + * returns the resulting unprotected content JWT instance. If the JWT creator set the (optional) + * {@link Header#getContentType() contentType} header value, the application may inspect that value to determine + * how to convert the byte array to the final content type as desired. * *

This is a convenience method that is usable if you are confident that the compact string argument reflects an - * unsigned plaintext JWT. An unsigned plaintext JWT has a String (non-JSON) body payload and it is not - * cryptographically signed.

+ * unprotected content JWT. An unprotected content JWT has a byte array payload and it is not + * cryptographically signed or encrypted. If the JWT creator set the (optional) + * {@link Header#getContentType() contentType} header value, the application may inspect that value to determine + * how to convert the byte array to the final content type as desired.

* - *

If the compact string presented does not reflect an unsigned plaintext JWT with non-JSON string body, + *

If the compact string presented does not reflect an unprotected content JWT with byte array payload, * an {@link UnsupportedJwtException} will be thrown.

* - * @param plaintextJwt a compact serialized unsigned plaintext JWT string. + * @param jwt a compact serialized unprotected content JWT string. * @return the {@link Jwt Jwt} instance that reflects the specified compact JWT string. - * @throws UnsupportedJwtException if the {@code plaintextJwt} argument does not represent an unsigned plaintext - * JWT - * @throws MalformedJwtException if the {@code plaintextJwt} string is not a valid JWT - * @throws SignatureException if the {@code plaintextJwt} string is actually a JWS and signature validation - * fails - * @throws IllegalArgumentException if the {@code plaintextJwt} string is {@code null} or empty or only whitespace + * @throws UnsupportedJwtException if the {@code jwt} argument does not represent an unprotected content JWT + * @throws MalformedJwtException if the {@code jwt} string is not a valid JWT + * @throws SignatureException if the {@code jwt} string is actually a JWS and signature validation fails + * @throws SecurityException if the {@code jwt} string is actually a JWE and decryption fails + * @throws IllegalArgumentException if the {@code jwt} string is {@code null} or empty or only whitespace * @see #parseClaimsJwt(String) - * @see #parsePlaintextJws(String) + * @see #parseContentJws(String) * @see #parseClaimsJws(String) * @see #parse(String, JwtHandler) * @see #parse(String) * @since 0.2 */ - Jwt parsePlaintextJwt(String plaintextJwt) - throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jwt parseContentJwt(String jwt) throws UnsupportedJwtException, MalformedJwtException, + SignatureException, SecurityException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and - * returns the resulting unsigned plaintext JWT instance. + * returns the resulting unprotected Claims JWT instance. * *

This is a convenience method that is usable if you are confident that the compact string argument reflects an - * unsigned Claims JWT. An unsigned Claims JWT has a {@link Claims} body and it is not cryptographically - * signed.

+ * unprotected Claims JWT. An unprotected Claims JWT has a {@link Claims} payload and it is not cryptographically + * signed or encrypted.

* - *

If the compact string presented does not reflect an unsigned Claims JWT, an + *

If the compact string presented does not reflect an unprotected Claims JWT, an * {@link UnsupportedJwtException} will be thrown.

* - * @param claimsJwt a compact serialized unsigned Claims JWT string. + * @param jwt a compact serialized unprotected Claims JWT string. * @return the {@link Jwt Jwt} instance that reflects the specified compact JWT string. - * @throws UnsupportedJwtException if the {@code claimsJwt} argument does not represent an unsigned Claims JWT - * @throws MalformedJwtException if the {@code claimsJwt} string is not a valid JWT - * @throws SignatureException if the {@code claimsJwt} string is actually a JWS and signature validation - * fails + * @throws UnsupportedJwtException if the {@code jwt} argument does not represent an unprotected Claims JWT + * @throws MalformedJwtException if the {@code jwt} string is not a valid JWT + * @throws SignatureException if the {@code jwt} string is actually a JWS and signature validation fails + * @throws SecurityException if the {@code jwt} string is actually a JWE and decryption fails * @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time * before the time this method is invoked. - * @throws IllegalArgumentException if the {@code claimsJwt} string is {@code null} or empty or only whitespace - * @see #parsePlaintextJwt(String) - * @see #parsePlaintextJws(String) + * @throws IllegalArgumentException if the {@code jwt} string is {@code null} or empty or only whitespace + * @see #parseContentJwt(String) + * @see #parseContentJws(String) * @see #parseClaimsJws(String) * @see #parse(String, JwtHandler) * @see #parse(String) * @since 0.2 */ - Jwt parseClaimsJwt(String claimsJwt) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jwt parseClaimsJwt(String jwt) throws ExpiredJwtException, UnsupportedJwtException, + MalformedJwtException, SignatureException, SecurityException, IllegalArgumentException; /** * Parses the specified compact serialized JWS string based on the builder's current configuration state and - * returns the resulting plaintext JWS instance. + * returns the resulting content JWS instance. If the JWT creator set the (optional) + * {@link Header#getContentType() contentType} header value, the application may inspect that value to determine + * how to convert the byte array to the final content type as desired. * *

This is a convenience method that is usable if you are confident that the compact string argument reflects a - * plaintext JWS. A plaintext JWS is a JWT with a String (non-JSON) body (payload) that has been - * cryptographically signed.

+ * content JWS. A content JWS is a JWT with a byte array payload that has been cryptographically signed.

* - *

If the compact string presented does not reflect a plaintext JWS, an {@link UnsupportedJwtException} + *

If the compact string presented does not reflect a content JWS, an {@link UnsupportedJwtException} * will be thrown.

* - * @param plaintextJws a compact serialized JWS string. + * @param jws a compact serialized JWS string. * @return the {@link Jws Jws} instance that reflects the specified compact JWS string. - * @throws UnsupportedJwtException if the {@code plaintextJws} argument does not represent an plaintext JWS - * @throws MalformedJwtException if the {@code plaintextJws} string is not a valid JWS - * @throws SignatureException if the {@code plaintextJws} JWS signature validation fails - * @throws IllegalArgumentException if the {@code plaintextJws} string is {@code null} or empty or only whitespace - * @see #parsePlaintextJwt(String) + * @throws UnsupportedJwtException if the {@code jws} argument does not represent a content JWS + * @throws MalformedJwtException if the {@code jws} string is not a valid JWS + * @throws SignatureException if the {@code jws} JWS signature validation fails + * @throws SecurityException if the {@code jws} string is actually a JWE and decryption fails + * @throws IllegalArgumentException if the {@code jws} string is {@code null} or empty or only whitespace + * @see #parseContentJwt(String) + * @see #parseContentJwe(String) * @see #parseClaimsJwt(String) * @see #parseClaimsJws(String) + * @see #parseClaimsJwe(String) * @see #parse(String, JwtHandler) * @see #parse(String) * @since 0.2 */ - Jws parsePlaintextJws(String plaintextJws) - throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jws parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException, + SecurityException, IllegalArgumentException; /** * Parses the specified compact serialized JWS string based on the builder's current configuration state and * returns the resulting Claims JWS instance. * *

This is a convenience method that is usable if you are confident that the compact string argument reflects a - * Claims JWS. A Claims JWS is a JWT with a {@link Claims} body that has been cryptographically signed.

+ * Claims JWS. A Claims JWS is a JWT with a {@link Claims} payload that has been cryptographically signed.

* *

If the compact string presented does not reflect a Claims JWS, an {@link UnsupportedJwtException} will be * thrown.

* - * @param claimsJws a compact serialized Claims JWS string. + * @param jws a compact serialized Claims JWS string. * @return the {@link Jws Jws} instance that reflects the specified compact Claims JWS string. * @throws UnsupportedJwtException if the {@code claimsJws} argument does not represent an Claims JWS * @throws MalformedJwtException if the {@code claimsJws} string is not a valid JWS * @throws SignatureException if the {@code claimsJws} JWS signature validation fails + * @throws SecurityException if the {@code jws} string is actually a JWE and decryption fails * @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time * before the time this method is invoked. * @throws IllegalArgumentException if the {@code claimsJws} string is {@code null} or empty or only whitespace - * @see #parsePlaintextJwt(String) + * @see #parseContentJwt(String) + * @see #parseContentJws(String) + * @see #parseContentJwe(String) * @see #parseClaimsJwt(String) - * @see #parsePlaintextJws(String) + * @see #parseClaimsJwe(String) * @see #parse(String, JwtHandler) * @see #parse(String) * @since 0.2 */ - Jws parseClaimsJws(String claimsJws) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + Jws parseClaimsJws(String jws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, + SignatureException, SecurityException, IllegalArgumentException; + + /** + * Parses the specified compact serialized JWE string based on the builder's current configuration state and + * returns the resulting content JWE instance. If the JWT creator set the (optional) + * {@link Header#getContentType() contentType} header value, the application may inspect that value to determine + * how to convert the byte array to the final content type as desired. + * + *

This is a convenience method that is usable if you are confident that the compact string argument reflects a + * content JWE. A content JWE is a JWT with a byte array payload that has been encrypted.

+ * + *

If the compact string presented does not reflect a content JWE, an {@link UnsupportedJwtException} + * will be thrown.

+ * + * @param jwe a compact serialized JWE string. + * @return the {@link Jwe Jwe} instance that reflects the specified compact JWE string. + * @throws UnsupportedJwtException if the {@code jwe} argument does not represent a content JWE + * @throws MalformedJwtException if the {@code jwe} string is not a valid JWE + * @throws SecurityException if the {@code jwe} JWE decryption fails + * @throws IllegalArgumentException if the {@code jwe} string is {@code null} or empty or only whitespace + * @see #parseContentJwt(String) + * @see #parseContentJws(String) + * @see #parseClaimsJwt(String) + * @see #parseClaimsJws(String) + * @see #parseClaimsJwe(String) + * @see #parse(String, JwtHandler) + * @see #parse(String) + * @since JJWT_RELEASE_VERSION + */ + Jwe parseContentJwe(String jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, + SecurityException, IllegalArgumentException; + + /** + * Parses the specified compact serialized JWE string based on the builder's current configuration state and + * returns the resulting Claims JWE instance. + * + *

This is a convenience method that is usable if you are confident that the compact string argument reflects a + * Claims JWE. A Claims JWE is a JWT with a {@link Claims} payload that has been encrypted.

+ * + *

If the compact string presented does not reflect a Claims JWE, an {@link UnsupportedJwtException} will be + * thrown.

+ * + * @param jwe a compact serialized Claims JWE string. + * @return the {@link Jwe Jwe} instance that reflects the specified compact Claims JWE string. + * @throws UnsupportedJwtException if the {@code claimsJwe} argument does not represent a Claims JWE + * @throws MalformedJwtException if the {@code claimsJwe} string is not a valid JWE + * @throws SignatureException if the {@code claimsJwe} JWE decryption fails + * @throws ExpiredJwtException if the specified JWT is a Claims JWE and the Claims has an expiration time + * before the time this method is invoked. + * @throws IllegalArgumentException if the {@code claimsJwe} string is {@code null} or empty or only whitespace + * @see #parseContentJwt(String) + * @see #parseContentJws(String) + * @see #parseContentJwe(String) + * @see #parseClaimsJwt(String) + * @see #parseClaimsJws(String) + * @see #parse(String, JwtHandler) + * @see #parse(String) + * @since JJWT_RELEASE_VERSION + */ + Jwe parseClaimsJwe(String jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, + SecurityException, IllegalArgumentException; } diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index a1513ea7..ed371b07 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -17,8 +17,16 @@ package io.jsonwebtoken; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Builder; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.StandardKeyAlgorithms; +import io.jsonwebtoken.security.StandardSecureDigestAlgorithms; import java.security.Key; +import java.security.Provider; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -26,8 +34,8 @@ import java.util.Map; * A builder to construct a {@link JwtParser}. Example usage: *
{@code
  *     Jwts.parserBuilder()
- *         .setSigningKey(...)
  *         .requireIssuer("https://issuer.example.com")
+ *         .verifyWith(...)
  *         .build()
  *         .parse(jwtString)
  * }
@@ -35,7 +43,65 @@ import java.util.Map; * @since 0.11.0 */ @SuppressWarnings("JavadocLinkAsPlainText") -public interface JwtParserBuilder { +public interface JwtParserBuilder extends Builder { + + /** + * Enables parsing of Unsecured JWSs (JWTs with an 'alg' (Algorithm) header value of + * 'none'). Be careful when calling this method - one should fully understand + * Unsecured JWS Security Considerations + * before enabling this feature. + *

If this method is not called, Unsecured JWSs are disabled by default as mandated by + * RFC 7518, Section + * 3.6.

+ * + * @return the builder for method chaining. + * @see Unsecured JWS Security Considerations + * @see Using the Algorithm "none" + * @see StandardSecureDigestAlgorithms#NONE + * @see #enableUnsecuredDecompression() + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder enableUnsecuredJws(); + + /** + * If {@link #enableUnsecuredJws() enabledUnsecuredJws} is enabled, calling this method additionally enables + * payload decompression of Unsecured JWSs (JWTs with an 'alg' (Algorithm) header value of 'none') that also have + * a 'zip' (Compression) header. This behavior is disabled by default because using compression + * algorithms with data from unverified (unauthenticated) parties can be susceptible to Denial of Service attacks + * and other data integrity problems as described in + * In the + * Compression Hornet’s Nest: A Security Study of Data Compression in Network Services. + * + *

Because this behavior is only relevant if {@link #enableUnsecuredJws() enabledUnsecuredJws} is specified, + * calling this method without also calling {@code enableUnsecuredJws()} will result in a build exception, as the + * incongruent state could reflect a misunderstanding of both behaviors which should be remedied by the + * application developer.

+ * + * As is the case for {@link #enableUnsecuredJws()}, be careful when calling this method - one should fully + * understand + * Unsecured JWS Security Considerations + * before enabling this feature. + * + * @return the builder for method chaining. + * @see Unsecured JWS Security Considerations + * @see In the + * Compression Hornet’s Nest: A Security Study of Data Compression in Network Services + * @see StandardSecureDigestAlgorithms#NONE + * @see #enableUnsecuredJws() + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder enableUnsecuredDecompression(); + + /** + * Sets the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} + * if the JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder setProvider(Provider provider); /** * Ensures that the specified {@code jti} exists in the parsed JWT. If missing or if the parsed @@ -156,8 +222,17 @@ public interface JwtParserBuilder { JwtParserBuilder setAllowedClockSkewSeconds(long seconds) throws IllegalArgumentException; /** - * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not - * a JWS (no signature), this key is not used. + *

Deprecation Notice

+ * + *

This method has been deprecated since JJWT_RELEASE_VERSION and will be removed before 1.0. It was not + * readily obvious to many JJWT users that this method was for bytes that pertained only to HMAC + * {@code SecretKey}s, and could be confused with keys of other types. It is better to obtain a type-safe + * {@link Key} instance and call the {@link #verifyWith(Key)} instead.

+ * + *

Previous Documentation

+ * + *

Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not + * a JWS (no signature), this key is not used.

* *

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

@@ -167,21 +242,13 @@ public interface JwtParserBuilder { * @param key the algorithm-specific signature verification key used to validate any discovered JWS digital * signature. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #verifyWith(Key)} for type safety and name congruence + * with the {@link #decryptWith(Key)} method. */ + @Deprecated JwtParserBuilder setSigningKey(byte[] key); /** - * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not - * a JWS (no signature), this key is not used. - * - *

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

- * - *

This method overwrites any previously set key.

- * - *

This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting - * byte array is used to invoke {@link #setSigningKey(byte[])}.

- * *

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

* *

This method has been deprecated because the {@code key} argument for this method can be confusing: keys for @@ -202,39 +269,140 @@ public interface JwtParserBuilder { * StackOverflow answer explaining why raw (non-base64-encoded) strings are almost always incorrect for * signature operations.

* - *

Finally, please use the {@link #setSigningKey(Key) setSigningKey(Key)} instead, as this method (and likely the - * {@code byte[]} variant) will be removed before the 1.0.0 release.

+ *

Finally, please use the {@link #verifyWith(Key)} method instead, as this method (and likely + * {@link #setSigningKey(byte[])}) will be removed before the 1.0.0 release.

* - * @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate - * any discovered JWS digital signature. + *

Previous JavaDoc

+ * + *

This is a convenience method that equates to the following:

+ * + *
+     * byte[] bytes = Decoders.{@link io.jsonwebtoken.io.Decoders#BASE64 BASE64}.decode(base64EncodedSecretKey);
+     * Key key = Keys.{@link io.jsonwebtoken.security.Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor}(bytes);
+     * return {@link #verifyWith(Key) verifyWith}(key);
+ * + * @param base64EncodedSecretKey BASE64-encoded HMAC-SHA key bytes used to create a Key which will be used to + * verify all encountered JWS digital signatures. * @return the parser builder for method chaining. - * @deprecated in favor of {@link #setSigningKey(Key)} as explained in the above Deprecation Notice, + * @deprecated in favor of {@link #verifyWith(Key)} as explained in the above Deprecation Notice, * and will be removed in 1.0.0. */ @Deprecated JwtParserBuilder setSigningKey(String base64EncodedSecretKey); /** - * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not - * a JWS (no signature), this key is not used. + *

Deprecation Notice

* - *

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

+ *

This method is being renamed to accurately reflect its purpose - the key is not technically a signing key, + * it is a signature verification key, and the two concepts can be different, especially with asymmetric key + * cryptography. The method has been deprecated since JJWT_RELEASE_VERSION in favor of + * {@link #verifyWith(Key)} for type safety, to reflect accurate naming of the concept, and for name congruence + * with the {@link #decryptWith(Key)} method.

* - *

This method overwrites any previously set key.

+ *

This method merely delegates directly to {@link #verifyWith(Key)}.

* - * @param key the algorithm-specific signature verification key to use to validate any discovered JWS digital - * signature. + * @param key the algorithm-specific signature verification key to use to verify all encountered JWS digital + * signatures. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #verifyWith(Key)} for naming congruence with the + * {@link #decryptWith(Key)} method. */ + @Deprecated JwtParserBuilder setSigningKey(Key key); /** - * Sets the {@link SigningKeyResolver} used to acquire the signing key that should be used to verify - * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used. + * Sets the signature verification key used to verify all encountered JWS signatures. If the encountered JWT + * string is not a JWS (e.g. unsigned or a JWE), this key is not used. + * + *

This is a convenience method to use in a specific scenario: when the parser will only ever encounter + * JWSs with signatures that can always be verified by a single key. This also implies that this key + * MUST be a valid key for the signature algorithm ({@code alg} header) used for the JWS.

+ * + *

If there is any chance that the parser will encounter JWSs + * that need different signature verification keys based on the JWS being parsed, or JWEs, it is strongly + * recommended to configure your own {@link Locator} via the + * {@link #setKeyLocator(Locator) setKeyLocator} method instead of using this one.

+ * + *

Calling this method overrides any previously set signature verification key.

+ * + * @param key the signature verification key to use to verify all encountered JWS digital signatures. + * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder verifyWith(Key key); + + /** + * Sets the decryption key to be used to decrypt all encountered JWEs. If the encountered JWT string is not a + * JWE (e.g. a JWS), this key is not used. + * + *

This is a convenience method to use in specific circumstances: when the parser will only ever encounter + * JWEs that can always be decrypted by a single key. This also implies that this key MUST be a valid + * key for both the key management algorithm ({@code alg} header) and the content encryption algorithm + * ({@code enc} header) used for the JWE.

+ * + *

If there is any chance that the parser will encounter JWEs that need different decryption keys based on the + * JWE being parsed, or JWSs, it is strongly recommended to configure + * your own {@link Locator Locator} via the {@link #setKeyLocator(Locator) setKeyLocator} method instead of + * using this one.

+ * + *

Calling this method overrides any previously set decryption key.

+ * + * @param key the algorithm-specific decryption key to use to decrypt all encountered JWEs. + * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder decryptWith(Key key); + + /** + * Sets the {@link Locator} used to acquire any signature verification or decryption key needed during parsing. + *
    + *
  • If the parsed String is a JWS, the {@code Locator} will be called to find the appropriate key + * necessary to verify the JWS signature.
  • + *
  • If the parsed String is a JWE, it will be called to find the appropriate decryption key.
  • + *
+ * + *

Specifying a key {@code Locator} is necessary when the signing or decryption key is not already known before + * parsing the JWT and the JWT header must be inspected first to determine how to + * look up the verification or decryption key. Once returned by the locator, the JwtParser will then either + * verify the JWS signature or decrypt the JWE payload with the returned key. For example:

+ * + *
+     * Jws<Claims> jws = Jwts.parserBuilder().setKeyLocator(new Locator<Key>() {
+     *         @Override
+     *         public Key locate(Header<?> header) {
+     *             if (header instanceof JwsHeader) {
+     *                 return getSignatureVerificationKey((JwsHeader)header); // implement me
+     *             } else {
+     *                 return getDecryptionKey((JweHeader)header); // implement me
+     *             }
+     *         }})
+     *     .build()
+     *     .parseClaimsJws(compact);
+     * 
+ * + *

A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.

+ * + * @param keyLocator the locator used to retrieve decryption or signature verification keys. + * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder setKeyLocator(Locator keyLocator); + + /** + *

Deprecation Notice

+ * + *

This method has been deprecated as of JJWT version JJWT_RELEASE_VERSION because it only supports key location + * for JWSs (signed JWTs) instead of both signed (JWS) and encrypted (JWE) scenarios. Use the + * {@link #setKeyLocator(Locator) setKeyLocator} method instead to ensure a locator that can work for both JWS and + * JWE inputs. This method will be removed for the 1.0 release.

+ * + *

Previous Documentation

+ * + *

Sets the {@link SigningKeyResolver} used to acquire the signing key that should be used to verify + * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used.

* *

Specifying a {@code SigningKeyResolver} is necessary when the signing key is not already known before parsing - * the JWT and the JWT header or payload (plaintext body or Claims) must be inspected first to determine how to + * the JWT and the JWT header or payload (content byte array or Claims) must be inspected first to determine how to * look up the signing key. Once returned by the resolver, the JwtParser will then verify the JWS signature with the * returned key. For example:

* @@ -250,22 +418,104 @@ public interface JwtParserBuilder { * *

A {@code SigningKeyResolver} is invoked once during parsing before the signature is verified.

* - *

This method should only be used if a signing key is not provided by the other {@code setSigningKey*} builder - * methods.

- * * @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #setKeyLocator(Locator)} */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResolver); + /** + * Adds the specified compression codecs to the parser's total set of supported compression codecs, + * overwriting any previously-added compression codecs with the same {@link CompressionCodec#getId() id}s. If the + * parser encounters a JWT {@code zip} header value that matches a compression codec's + * {@link CompressionCodec#getId() CompressionCodec.getId()}, that codec will be used for decompression. + * + *

There may be only one registered {@code CompressionCodec} per {@code id}, and the {@code codecs} + * collection is added in iteration order; if a duplicate id is found when iterating the {@code codecs} + * collection, the later element will evict any previously-added algorithm with the same {@code id}.

+ * + *

Finally, {@link CompressionCodecs#DEFLATE} and {@link CompressionCodecs#GZIP} are added last, + * after those in the {@code codecs} collection, to ensure that JWA standard algorithms cannot be + * accidentally replaced.

+ * + *

This method is a simpler alternative than creating and registering a custom locator via the + * {@link #setCompressionCodecLocator(Locator)} method.

+ * + * @param codecs collection of compression codecs to add to the parser's total set of supported compression codecs. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder addCompressionCodecs(Collection codecs); + + /** + * Adds the specified AEAD encryption algorithms to the parser's total set of supported encryption algorithms, + * overwriting any previously-added algorithms with the same {@link AeadAlgorithm#getId() id}s. + * + *

There may be only one registered {@code AeadAlgorithm} per algorithm {@code id}, and the {@code encAlgs} + * collection is added in iteration order; if a duplicate id is found when iterating the {@code encAlgs} + * collection, the later element will evict any previously-added algorithm with the same {@code id}.

+ * + *

Finally, the {@link Jwts#ENC JWA standard encryption algorithms} are added last, + * after those in the {@code encAlgs} collection, to ensure that JWA standard algorithms cannot be + * accidentally replaced.

+ * + * @param encAlgs collection of AEAD encryption algorithms to add to the parser's total set of supported + * encryption algorithms. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs); + + /** + * Adds the specified signature algorithms to the parser's total set of supported signature algorithms, + * overwriting any previously-added algorithms with the same + * {@link Identifiable#getId() id}s. + * + *

There may be only one registered {@code SecureDigestAlgorithm} per algorithm {@code id}, and the + * {@code sigAlgs} collection is added in iteration order; if a duplicate id is found when iterating the + * {@code sigAlgs} collection, the later element will evict any previously-added algorithm with the same + * {@code id}.

+ * + *

Finally, the {@link Jwts#SIG JWA standard signature and MAC algorithms} are + * added last, after those in the {@code sigAlgs} collection, to ensure that JWA standard algorithms + * cannot be accidentally replaced.

+ * + * @param sigAlgs collection of signing algorithms to add to the parser's total set of supported signature + * algorithms. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs); + + /** + * Adds the specified key management algorithms to the parser's total set of supported key algorithms, + * overwriting any previously-added algorithms with the same {@link KeyAlgorithm#getId() id}s. + * + *

There may be only one registered {@code KeyAlgorithm} per algorithm {@code id}, and the {@code keyAlgs} + * collection is added in iteration order; if a duplicate id is found when iterating the {@code keyAlgs} + * collection, the later element will evict any previously-added algorithm with the same {@code id}.

+ * + *

Finally, the {@link StandardKeyAlgorithms#values() JWA standard key management algorithms} + * are added last, after those in the {@code keyAlgs} collection, to ensure that JWA standard algorithms + * cannot be accidentally replaced.

+ * + * @param keyAlgs collection of key management algorithms to add to the parser's total set of supported key + * management algorithms. + * @return the builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs); + /** * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. * - *

NOTE: Compression is not defined by the JWT Specification, and it is not expected that other libraries - * (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. This method is only - * useful if the compact JWT was compressed with JJWT >= 0.6.0 or another library that you know implements - * the same behavior.

+ *

NOTE: Compression is not defined by the JWS Specification - only the JWE Specification - and it is + * not expected that other libraries (including JJWT versions < 0.6.0) are able to consume a compressed JWS + * body correctly. This method is only useful if the compact JWS was compressed with JJWT >= 0.6.0 or + * another library that you know implements the same behavior.

* *

Default Support

* @@ -274,15 +524,55 @@ public interface JwtParserBuilder { * and {@link CompressionCodecs#GZIP GZIP} algorithms by default - you do not need to * specify a {@code CompressionCodecResolver} in these cases.

* - *

However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement - * your own {@link CompressionCodecResolver} and specify that via this method and also when + *

However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must + * implement your own {@link CompressionCodecResolver} and specify that via this method and also when * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} JWTs.

* * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. * @return the parser builder for method chaining. + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #setCompressionCodecLocator(Locator)} to use the + * congruent {@code Locator} concept used elsewhere (such as {@link #setKeyLocator(Locator)}). */ + @Deprecated JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); + /** + * Sets the {@link CompressionCodec} {@code Locator} used to acquire the {@code CompressionCodec} that should be + * used to decompress the JWT body. + * + *

NOTE: Compression is not defined by the JWS Specification - only the JWE Specification - and it is + * not expected that other libraries (including JJWT versions < 0.6.0) are able to consume a compressed JWS + * body correctly. This method is only useful if the compact JWS was compressed with JJWT >= 0.6.0 or + * another library that you know implements the same behavior.

+ * + *

Simple Registration

+ * + *

If a CompressionCodec can be resolved in the JWT Header via a simple {@code zip} header value lookup, it is + * recommended to call the {@link #addCompressionCodecs(Collection)} method instead of this one. That method + * will add the codec to the total set of supported codecs and lookup will achieved by matching the + * {@link CompressionCodec#getId() CompressionCodec.getId()} against the {@code zip} header value automatically.

+ * + *

You only need to call this method with a custom locator if compression codec lookup cannot be based on the + * {@code zip} header value.

+ * + *

Default Support

+ * + *

JJWT's default {@link JwtParser} implementation supports both the + * {@link CompressionCodecs#DEFLATE DEFLATE} + * and {@link CompressionCodecs#GZIP GZIP} algorithms by default - you do not need to + * specify a {@code CompressionCodec} {@link Locator} in these cases.

+ * + *

However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, and + * {@link #addCompressionCodecs(Collection)} is not sufficient, you must + * implement your own {@code CompressionCodec} {@link Locator} and specify that via this method and also when + * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} JWTs.

+ * + * @param locator the compression codec locator used to decompress the JWT body. + * @return the parser builder for method chaining. + * @since JJWT_RELEASE_VERSION + */ + JwtParserBuilder setCompressionCodecLocator(Locator locator); + /** * Perform Base64Url decoding with the specified Decoder * diff --git a/api/src/main/java/io/jsonwebtoken/Jwts.java b/api/src/main/java/io/jsonwebtoken/Jwts.java index 4dd75ccc..edfef680 100644 --- a/api/src/main/java/io/jsonwebtoken/Jwts.java +++ b/api/src/main/java/io/jsonwebtoken/Jwts.java @@ -15,7 +15,12 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.lang.Builder; import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Registry; +import io.jsonwebtoken.security.StandardEncryptionAlgorithms; +import io.jsonwebtoken.security.StandardKeyAlgorithms; +import io.jsonwebtoken.security.StandardSecureDigestAlgorithms; import java.util.Map; @@ -23,58 +28,188 @@ import java.util.Map; * Factory class useful for creating instances of JWT interfaces. Using this factory class can be a good * alternative to tightly coupling your code to implementation classes. * + *

Standard Algorithm References

+ *

Standard JSON Web Token algorithms used during JWS or JWE building or parsing are available organized by + * algorithm type. Each organized collection of algorithms is available via a constant to allow + * for easy code-completion in IDEs, showing available algorithm instances. For example, when typing:

+ *
+ * Jwts.// press code-completion hotkeys to suggest available algorithm registry fields
+ * Jwts.{@link #SIG}.// press hotkeys to suggest individual Digital Signature or MAC algorithms or utility methods
+ * Jwts.{@link #ENC}.// press hotkeys to suggest individual encryption algorithms or utility methods
+ * Jwts.{@link #KEY}.// press hotkeys to suggest individual key algorithms or utility methods
+ * * @since 0.1 */ public final class Jwts { + @SuppressWarnings("rawtypes") private static final Class[] MAP_ARG = new Class[]{Map.class}; + /** + * All JWA (RFC 7518) standard Cryptographic + * Algorithms for Content Encryption defined in the + * + * JSON Web Signature and Encryption Algorithms Registry. In addition to its + * {@link Registry#get(Object) get} and {@link Registry#find(Object) find} lookup methods, each standard algorithm + * is also available as a ({@code public final}) constant for direct type-safe reference in application code. + * For example: + *
+     * Jwts.builder()
+     *    // ... etc ...
+     *    .encryptWith(aKey, Jwts.ENC.A256GCM) // or A128GCM, A192GCM, etc...
+     *    .build();
+ * + * @since JJWT_RELEASE_VERSION + */ + public static final StandardEncryptionAlgorithms ENC = StandardEncryptionAlgorithms.get(); + + /** + * All JWA (RFC 7518) standard Cryptographic + * Algorithms for Digital Signatures and MACs defined in the + * JSON Web Signature and Encryption Algorithms + * Registry. In addition to its + * {@link Registry#get(Object) get} and {@link Registry#find(Object) find} lookup methods, each standard algorithm + * is also available as a ({@code public final}) constant for direct type-safe reference in application code. + * For example: + *
+     * Jwts.builder()
+     *    // ... etc ...
+     *    .signWith(aKey, Jwts.SIG.HS512) // or RS512, PS256, EdDSA, etc...
+     *    .build();
+ * + * @since JJWT_RELEASE_VERSION + */ + public static final StandardSecureDigestAlgorithms SIG = StandardSecureDigestAlgorithms.get(); + + /** + * All JWA (RFC 7518) standard Cryptographic + * Algorithms for Key Management. In addition to its + * convenience {@link Registry#get(Object) get} and {@link Registry#find(Object) find} lookup methods, each + * standard algorithm is also available as a ({@code public final}) constant for direct type-safe reference in + * application code. For example: + *
+     * Jwts.builder()
+     *    // ... etc ...
+     *    .encryptWith(aKey, Jwts.KEY.ECDH_ES_A256KW, Jwts.ENC.A256GCM)
+     *    .build();
+ * + * @since JJWT_RELEASE_VERSION + */ + public static final StandardKeyAlgorithms KEY = StandardKeyAlgorithms.get(); + + /** + * Private constructor, prevent instantiation. + */ private Jwts() { } /** - * Creates a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs. As this - * is a less common use of JWTs, consider using the {@link #jwsHeader()} factory method instead if you will later - * digitally sign the JWT. + *

Deprecation Notice: Renamed from {@code header} to {@code unprotectedHeader} since + * JJWT_RELEASE_VERSION and deprecated in favor of {@link #header()} as + * the updated builder-based method supports method chaining and is capable of automatically constructing + * {@link UnprotectedHeader}, {@link JwsHeader}, and {@link JweHeader} automatically based on builder state.

* - * @return a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs. + *

Previous Documentation

+ *

Creates a new {@link UnprotectedHeader} instance suitable for unprotected (not digitally signed or encrypted) + * JWTs. Because {@code Header} extends {@link Map} and map mutation methods cannot support method chaining, + * consider using the more flexible {@link #header()} method instead, which does support method + * chaining and other builder conveniences not available on the {@link UnprotectedHeader} interface.

+ * + * @return a new {@link UnprotectedHeader} instance suitable for unprotected (not digitally signed or + * encrypted) JWTs. + * @see #header() + * @since JJWT_RELEASE_VERSION + * @deprecated since JJWT_RELEASE_VERSION. This method was created to rename the previous {@code header} + * method, but header construction should now use {@link #header()}. This method will be removed in a future + * release before 1.0. */ - public static Header header() { - return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader"); + @Deprecated + public static UnprotectedHeader unprotectedHeader() { + return Classes.newInstance("io.jsonwebtoken.impl.DefaultUnprotectedHeader"); } /** - * Creates a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs, populated - * with the specified name/value pairs. As this is a less common use of JWTs, consider using the - * {@link #jwsHeader(java.util.Map)} factory method instead if you will later digitally sign the JWT. + *

Deprecation Notice: deprecated since JJWT_RELEASE_VERSION in favor of {@link #header()} as + * the newer method supports method chaining and is capable of automatically constructing + * {@link UnprotectedHeader}, {@link JwsHeader}, and {@link JweHeader} automatically based on builder state.

* - * @param header map of name/value pairs used to create a plaintext (not digitally signed) JWT + *

Previous Documentation

+ *

Creates a new {@link UnprotectedHeader} instance suitable for unprotected (not digitally signed or encrypted) + * JWTs, populated with the specified name/value pairs. Because {@code Header} extends {@link Map} and map + * mutation methods cannot support method chaining, consider using the more flexible {@link #header()} + * method instead, which does support method chaining and other builder conveniences not available on the + * {@link UnprotectedHeader} interface.

+ * + * @param header map of name/value pairs used to create an unprotected (not digitally signed or encrypted) JWT * {@code Header} instance. - * @return a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs. + * @return a new {@link UnprotectedHeader} instance suitable for unprotected (not digitally signed or encrypted) + * JWTs. + * @see #header() + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #header()} as the builder supports + * method chaining and is more flexible and powerful. This method will be removed in a future release before 1.0. */ - public static Header header(Map header) { - return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader", MAP_ARG, header); + @Deprecated + public static UnprotectedHeader header(Map header) { + return Classes.newInstance("io.jsonwebtoken.impl.DefaultUnprotectedHeader", MAP_ARG, header); } /** - * Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's). + * Returns a new {@link DynamicHeaderBuilder} that can build any type of {@link Header} instance depending on + * which builder properties are set. + * + * @return a new {@link DynamicHeaderBuilder} that can build any type of {@link Header} instance depending on + * which builder properties are set. + * @since JJWT_RELEASE_VERSION + */ + public static DynamicHeaderBuilder header() { + return Classes.newInstance("io.jsonwebtoken.impl.DefaultDynamicHeaderBuilder"); + } + + /** + *

Deprecation Notice: deprecated since JJWT_RELEASE_VERSION in favor of {@link #header()} as + * the newer method supports method chaining and is capable of automatically constructing + * {@link UnprotectedHeader}, {@link JwsHeader}, and {@link JweHeader} automatically based on builder state.

+ * + *

Previous Documentation

+ *

Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's). Because {@code Header} + * extends {@link Map} and map mutation methods cannot support method chaining, consider using the + * more flexible {@link #header()} method instead, which does support method chaining, as well as other + * convenience builder methods not available via the {@link JwsHeader} interface.

* * @return a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's). + * @see #header() * @see JwtBuilder#setHeader(Header) + * @see JwtBuilder#setHeader(Builder) + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #header()} as the builder supports + * method chaining and is more flexible and powerful. This method will be removed in a future release before 1.0. */ + @Deprecated public static JwsHeader jwsHeader() { return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader"); } /** - * Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the - * specified name/value pairs. + *

Deprecation Notice: deprecated since JJWT_RELEASE_VERSION in favor of {@link #header()} as + * the newer method supports method chaining and is capable of automatically constructing + * {@link UnprotectedHeader}, {@link JwsHeader}, and {@link JweHeader} automatically based on builder state.

+ * + *

Previous Documentation

+ *

Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the + * specified name/value pairs. Because {@code Header} extends {@link Map} and map mutation methods cannot + * support method chaining, consider using the more flexible {@link #header()} method instead, + * which does support method chaining and other builder conveniences not available on the + * {@link JwsHeader} interface directly.

* * @param header map of name/value pairs used to create a new {@link JwsHeader} instance. * @return a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the * specified name/value pairs. + * @see #header() * @see JwtBuilder#setHeader(Header) + * @see JwtBuilder#setHeader(Builder) + * @deprecated since JJWT_RELEASE_VERSION in favor of {@link #header()} as the builder supports + * method chaining and is more flexible and powerful. This method will be removed in a future release before 1.0. */ + @Deprecated public static JwsHeader jwsHeader(Map header) { return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader", MAP_ARG, header); } diff --git a/api/src/main/java/io/jsonwebtoken/Locator.java b/api/src/main/java/io/jsonwebtoken/Locator.java new file mode 100644 index 00000000..ae64ba2d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/Locator.java @@ -0,0 +1,41 @@ +/* + * 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; + +import java.security.Key; + +/** + * A {@link Locator} can return an object referenced in a JWT {@link Header} that is necessary to process + * the associated JWT. + * + *

For example, a {@code Locator} implementation can inspect a header's {@code kid} (Key ID) parameter, and use the + * discovered {@code kid} value to lookup and return the associated {@link Key} instance. JJWT could then use this + * {@code key} to decrypt a JWE or verify a JWS signature.

+ * + * @param the type of object that may be returned from the {@link #locate(Header)} method + * @since JJWT_RELEASE_VERSION + */ +public interface Locator { + + /** + * Returns an object referenced in the specified {@code header}, or {@code null} if the object couldn't be found. + * + * @param header the JWT header to inspect; may be an instance of {@link Header}, {@link JwsHeader} or + * {@link JweHeader} depending on if the respective JWT is an unprotected JWT, JWS or JWE. + * @return an object referenced in the specified {@code header}, or {@code null} if the object couldn't be found. + */ + T locate(Header header); +} diff --git a/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java new file mode 100644 index 00000000..c82a5a74 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/LocatorAdapter.java @@ -0,0 +1,112 @@ +/* + * 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; + +import io.jsonwebtoken.lang.Assert; + +/** + * Adapter pattern implementation for the {@link Locator} interface. Subclasses can override any of the + * {@link #locate(UnprotectedHeader)}, {@link #locate(ProtectedHeader)}, {@link #locate(JwsHeader)}, or + * {@link #locate(JweHeader)} methods for type-specific logic if desired when the encountered header is an + * unprotected JWT, or an integrity-protected JWT (either a JWS or JWE). + * + * @since JJWT_RELEASE_VERSION + */ +public abstract class LocatorAdapter implements Locator { + + /** + * Constructs a new instance, where all default method implementations return {@code null}. + */ + public LocatorAdapter() { + } + + /** + * Inspects the specified header, and delegates to the {@link #locate(UnprotectedHeader)} method if the header + * is an {@link UnprotectedHeader} or the {@link #locate(ProtectedHeader)} method if the header is either a + * {@link JwsHeader} or {@link JweHeader}. + * + * @param header the JWT header to inspect; may be an instance of {@link UnprotectedHeader}, {@link JwsHeader} or + * {@link JweHeader} depending on if the respective JWT is an unprotected JWT, JWS or JWE. + * @return an object referenced in the specified header, or {@code null} if the referenced object cannot be found + * or does not exist. + */ + @Override + public final T locate(Header header) { + Assert.notNull(header, "Header cannot be null."); + if (header instanceof ProtectedHeader) { + ProtectedHeader protectedHeader = (ProtectedHeader) header; + return locate(protectedHeader); + } else { + Assert.isInstanceOf(UnprotectedHeader.class, header, "Unrecognized Header type."); + return locate((UnprotectedHeader) header); + } + } + + /** + * Returns an object referenced in the specified {@link ProtectedHeader}, or {@code null} if the referenced + * object cannot be found or does not exist. This is a convenience method that delegates to + * {@link #locate(JwsHeader)} if the {@code header} is a {@link JwsHeader} or {@link #locate(JweHeader)} if the + * {@code header} is a {@link JweHeader}. + * + * @param header the protected header of an encountered JWS or JWE. + * @return an object referenced in the specified {@link ProtectedHeader}, or {@code null} if the referenced + * object cannot be found or does not exist. + */ + protected T locate(ProtectedHeader header) { + if (header instanceof JwsHeader) { + return locate((JwsHeader) header); + } else { + Assert.isInstanceOf(JweHeader.class, header, "Unrecognized ProtectedHeader type."); + return locate((JweHeader) header); + } + } + + /** + * Returns an object referenced in the specified JWE header, or {@code null} if the referenced + * object cannot be found or does not exist. Default implementation simply returns {@code null}. + * + * @param header the header of an encountered JWE. + * @return an object referenced in the specified JWE header, or {@code null} if the referenced + * object cannot be found or does not exist. + */ + protected T locate(JweHeader header) { + return null; + } + + /** + * Returns an object referenced in the specified JWS header, or {@code null} if the referenced + * object cannot be found or does not exist. Default implementation simply returns {@code null}. + * + * @param header the header of an encountered JWS. + * @return an object referenced in the specified JWS header, or {@code null} if the referenced + * object cannot be found or does not exist. + */ + protected T locate(JwsHeader header) { + return null; + } + + /** + * Returns an object referenced in the specified Unprotected JWT header, or {@code null} if the referenced + * object cannot be found or does not exist. Default implementation simply returns {@code null}. + * + * @param header the header of an encountered JWE. + * @return an object referenced in the specified Unprotected JWT header, or {@code null} if the referenced + * object cannot be found or does not exist. + */ + protected T locate(UnprotectedHeader header) { + return null; + } +} diff --git a/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java b/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java index 6490550a..5729388d 100644 --- a/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/MalformedJwtException.java @@ -22,10 +22,21 @@ package io.jsonwebtoken; */ public class MalformedJwtException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public MalformedJwtException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public MalformedJwtException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/MissingClaimException.java b/api/src/main/java/io/jsonwebtoken/MissingClaimException.java index 030fe98d..7582da06 100644 --- a/api/src/main/java/io/jsonwebtoken/MissingClaimException.java +++ b/api/src/main/java/io/jsonwebtoken/MissingClaimException.java @@ -22,11 +22,32 @@ package io.jsonwebtoken; * @since 0.6 */ public class MissingClaimException extends InvalidClaimException { - public MissingClaimException(Header header, Claims claims, String message) { - super(header, claims, message); + + /** + * Creates a new instance with the specified explanation message. + * + * @param header the header associated with the claims that did not contain the required claim + * @param claims the claims that did not contain the required claim + * @param claimName the name of the claim that could not be validated + * @param claimValue the value of the claim that could not be validated + * @param message the message explaining why the exception is thrown. + */ + public MissingClaimException(Header header, Claims claims, String claimName, Object claimValue, String message) { + super(header, claims, claimName, claimValue, message); } - public MissingClaimException(Header header, Claims claims, String message, Throwable cause) { - super(header, claims, message, cause); + + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param header the header associated with the claims that did not contain the required claim + * @param claims the claims that did not contain the required claim + * @param claimName the name of the claim that could not be validated + * @param claimValue the value of the claim that could not be validated + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public MissingClaimException(Header header, Claims claims, String claimName, Object claimValue, String message, Throwable cause) { + super(header, claims, claimName, claimValue, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java b/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java index 8853832e..f6045730 100644 --- a/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/PrematureJwtException.java @@ -22,18 +22,27 @@ package io.jsonwebtoken; */ public class PrematureJwtException extends ClaimJwtException { - public PrematureJwtException(Header header, Claims claims, String message) { + /** + * Creates a new instance with the specified explanation message. + * + * @param header jwt header + * @param claims jwt claims (body) + * @param message the message explaining why the exception is thrown. + */ + public PrematureJwtException(Header header, Claims claims, String message) { super(header, claims, message); } /** - * @param header jwt header - * @param claims jwt claims (body) + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param header jwt header + * @param claims jwt claims (body) * @param message exception message - * @param cause cause + * @param cause cause * @since 0.5 */ - public PrematureJwtException(Header header, Claims claims, String message, Throwable cause) { + public PrematureJwtException(Header header, Claims claims, String message, Throwable cause) { super(header, claims, message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java new file mode 100644 index 00000000..1483764e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeader.java @@ -0,0 +1,86 @@ +/* + * 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; + +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.X509Accessor; + +import java.net.URI; +import java.util.Set; + +/** + * A JWT header that is integrity protected, either by JWS digital signature or JWE AEAD encryption. + * + * @param The exact header subtype returned during mutation (setter) operations. + * @see JwsHeader + * @see JweHeader + * @since JJWT_RELEASE_VERSION + */ +public interface ProtectedHeader> extends Header, ProtectedHeaderMutator, X509Accessor { + + /** + * Returns the {@code jku} (JWK Set URL) value that refers to a + * JWK Set + * resource containing JSON-encoded Public Keys, or {@code null} if not present. When present in a + * {@link JwsHeader}, the first public key in the JWK Set must be the public key complement of the private + * key used to sign the JWS. When present in a {@link JweHeader}, the first public key in the JWK Set must + * be the public key used during encryption. + * + * @return a URI that refers to a JWK Set + * resource for a set of JSON-encoded Public Keys, or {@code null} if not present. + * @see JWS JWK Set URL + * @see JWE JWK Set URL + */ + URI getJwkSetUrl(); + + /** + * Returns the {@code jwk} (JSON Web Key) associated with the JWT. When present in a {@link JwsHeader}, the + * {@code jwk} is the public key complement of the private key used to digitally sign the JWS. When present in a + * {@link JweHeader}, the {@code jwk} is the public key to which the JWE was encrypted, and may be used to + * determine the private key needed to decrypt the JWE. + * + * @return the {@code jwk} (JSON Web Key) associated with the header. + * @see JWS {@code jwk} (JSON Web Key) Header Parameter + * @see JWE {@code jwk} (JSON Web Key) Header Parameter + */ + PublicJwk getJwk(); + + /** + * Returns the JWT case-sensitive {@code kid} (Key ID) header value or {@code null} if not present. + * + *

The keyId header parameter is a hint indicating which key was used to secure a JWS or JWE. This + * parameter allows originators to explicitly signal a change of key to recipients. The structure of the keyId + * value is unspecified. Its value is a CaSe-SeNsItIvE string.

+ * + *

When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

+ * + * @return the case-sensitive {@code kid} header value or {@code null} if not present. + * @see JWS Key ID + * @see JWE Key ID + */ + String getKeyId(); + + /** + * Returns the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient, or {@code null} if not present. + * + * @return the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient, or {@code null} if not present. + * @see JWS {@code crit} (Critical) Header Parameter + * @see JWS {@code crit} (Critical) Header Parameter + */ + Set getCritical(); +} diff --git a/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java new file mode 100644 index 00000000..3c4a8ef9 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java @@ -0,0 +1,90 @@ +/* + * 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; + +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.X509Mutator; + +import java.net.URI; +import java.util.Set; + +/** + * Mutation (modifications) to a {@link ProtectedHeader Header} instance. + * + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface ProtectedHeaderMutator> extends HeaderMutator, X509Mutator { + + /** + * Sets the {@code jku} (JWK Set URL) value that refers to a + * JWK Set + * resource containing JSON-encoded Public Keys, or {@code null} if not present. When set for a + * {@link JwsHeader}, the first public key in the JWK Set must be the public key complement of the + * private key used to sign the JWS. When set for a {@link JweHeader}, the first public key in the JWK Set + * must be the public key used during encryption. + * + * @param uri a URI that refers to a JWK Set + * resource containing JSON-encoded Public Keys + * @return the header for method chaining + * @see JWS JWK Set URL + * @see JWE JWK Set URL + */ + T setJwkSetUrl(URI uri); + + /** + * Sets the {@code jwk} (JSON Web Key) associated with the JWT. When set for a {@link JwsHeader}, the + * {@code jwk} is the public key complement of the private key used to digitally sign the JWS. When set for a + * {@link JweHeader}, the {@code jwk} is the public key to which the JWE was encrypted, and may be used to + * determine the private key needed to decrypt the JWE. + * + * @param jwk the {@code jwk} (JSON Web Key) associated with the header. + * @return the header for method chaining + * @see JWS jwk (JSON Web Key) Header Parameter + * @see JWE jwk (JSON Web Key) Header Parameter + */ + T setJwk(PublicJwk jwk); + + /** + * Sets the JWT case-sensitive {@code kid} (Key ID) header value. A {@code null} value will remove the property + * from the JSON map. + * + *

The keyId header parameter is a hint indicating which key was used to secure a JWS or JWE. This parameter + * allows originators to explicitly signal a change of key to recipients. The structure of the keyId value is + * unspecified. Its value MUST be a case-sensitive string.

+ * + *

When used with a JWK, the keyId value is used to match a JWK {@code keyId} parameter value.

+ * + * @param kid the case-sensitive JWS {@code kid} header value or {@code null} to remove the property from the JSON map. + * @return the header instance for method chaining. + * @see JWS Key ID + * @see JWE Key ID + */ + T setKeyId(String kid); + + /** + * Sets the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient. A {@code null} value will remove the + * property from the JSON map. + * + * @param crit the header parameter names that use extensions to the JWT or JWA specification that MUST + * be understood and supported by the JWT recipient. + * @return the header for method chaining. + * @see JWS crit (Critical) Header Parameter + * @see JWS crit (Critical) Header Parameter + */ + T setCritical(Set crit); +} diff --git a/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java b/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java index eeb60d30..77a0035d 100644 --- a/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java +++ b/api/src/main/java/io/jsonwebtoken/RequiredTypeException.java @@ -16,16 +16,28 @@ package io.jsonwebtoken; /** - * Exception thrown when {@link Claims#get(String, Class)} is called and the value does not match the type of the - * {@code Class} argument. + * Exception thrown when attempting to obtain a value from a JWT or JWK and the existing value does not match the + * expected type. * * @since 0.6 */ public class RequiredTypeException extends JwtException { + + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public RequiredTypeException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public RequiredTypeException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java index b61138ff..7c2a8b33 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java @@ -18,6 +18,7 @@ package io.jsonwebtoken; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.StandardSecureDigestAlgorithms; import io.jsonwebtoken.security.WeakKeyException; import javax.crypto.SecretKey; @@ -34,7 +35,9 @@ import java.util.List; * JSON Web Algorithms specification. * * @since 0.1 + * @deprecated since JJWT_RELEASE_VERSION; use {@link StandardSecureDigestAlgorithms} instead. */ +@Deprecated public enum SignatureAlgorithm { /** @@ -110,10 +113,10 @@ public enum SignatureAlgorithm { //purposefully ordered higher to lower: private static final List PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList( - SignatureAlgorithm.HS512, SignatureAlgorithm.HS384, SignatureAlgorithm.HS256)); + SignatureAlgorithm.HS512, SignatureAlgorithm.HS384, SignatureAlgorithm.HS256)); //purposefully ordered higher to lower: private static final List PREFERRED_EC_ALGS = Collections.unmodifiableList(Arrays.asList( - SignatureAlgorithm.ES512, SignatureAlgorithm.ES384, SignatureAlgorithm.ES256)); + SignatureAlgorithm.ES512, SignatureAlgorithm.ES384, SignatureAlgorithm.ES256)); private final String value; private final String description; @@ -132,7 +135,7 @@ public enum SignatureAlgorithm { SignatureAlgorithm(String value, String description, String familyName, String jcaName, boolean jdkStandard, int digestLength, int minKeyLength) { - this(value, description,familyName, jcaName, jdkStandard, digestLength, minKeyLength, jcaName); + this(value, description, familyName, jcaName, jdkStandard, digestLength, minKeyLength, jcaName); } SignatureAlgorithm(String value, String description, String familyName, String jcaName, boolean jdkStandard, @@ -365,25 +368,25 @@ public enum SignatureAlgorithm { // These next checks use equalsIgnoreCase per https://github.com/jwtk/jjwt/issues/381#issuecomment-412912272 if (!HS256.jcaName.equalsIgnoreCase(alg) && - !HS384.jcaName.equalsIgnoreCase(alg) && - !HS512.jcaName.equalsIgnoreCase(alg) && - !HS256.pkcs12Name.equals(alg) && - !HS384.pkcs12Name.equals(alg) && - !HS512.pkcs12Name.equals(alg)) { + !HS384.jcaName.equalsIgnoreCase(alg) && + !HS512.jcaName.equalsIgnoreCase(alg) && + !HS256.pkcs12Name.equals(alg) && + !HS384.pkcs12Name.equals(alg) && + !HS512.pkcs12Name.equals(alg)) { throw new InvalidKeyException("The " + keyType(signing) + " key's algorithm '" + alg + - "' does not equal a valid HmacSHA* algorithm name and cannot be used with " + name() + "."); + "' does not equal a valid HmacSHA* algorithm name and cannot be used with " + name() + "."); } int size = encoded.length * 8; //size in bits if (size < this.minKeyLength) { String msg = "The " + keyType(signing) + " key's size is " + size + " bits which " + - "is not secure enough for the " + name() + " algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + name() + " MUST have a " + - "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the " + Keys.class.getName() + " class's " + - "'secretKeyFor(SignatureAlgorithm." + name() + ")' method to create a key guaranteed to be " + - "secure enough for " + name() + ". See " + - "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + "is not secure enough for the " + name() + " algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + name() + " MUST have a " + + "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the " + Keys.class.getName() + " class's " + + "'secretKeyFor(SignatureAlgorithm." + name() + ")' method to create a key guaranteed to be " + + "secure enough for " + name() + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; throw new WeakKeyException(msg); } @@ -407,13 +410,13 @@ public enum SignatureAlgorithm { int size = ecKey.getParams().getOrder().bitLength(); if (size < this.minKeyLength) { String msg = "The " + keyType(signing) + " key's size (ECParameterSpec order) is " + size + - " bits which is not secure enough for the " + name() + " algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.4) states that keys used with " + - name() + " MUST have a size >= " + this.minKeyLength + - " bits. Consider using the " + Keys.class.getName() + " class's " + - "'keyPairFor(SignatureAlgorithm." + name() + ")' method to create a key pair guaranteed " + - "to be secure enough for " + name() + ". See " + - "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; + " bits which is not secure enough for the " + name() + " algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.4) states that keys used with " + + name() + " MUST have a size >= " + this.minKeyLength + + " bits. Consider using the " + Keys.class.getName() + " class's " + + "'keyPairFor(SignatureAlgorithm." + name() + ")' method to create a key pair guaranteed " + + "to be secure enough for " + name() + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; throw new WeakKeyException(msg); } @@ -431,12 +434,12 @@ public enum SignatureAlgorithm { String section = name().startsWith("P") ? "3.5" : "3.3"; String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + - "enough for the " + name() + " algorithm. The JWT JWA Specification (RFC 7518, Section " + - section + ") states that keys used with " + name() + " MUST have a size >= " + - this.minKeyLength + " bits. Consider using the " + Keys.class.getName() + " class's " + - "'keyPairFor(SignatureAlgorithm." + name() + ")' method to create a key pair guaranteed " + - "to be secure enough for " + name() + ". See " + - "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + "enough for the " + name() + " algorithm. The JWT JWA Specification (RFC 7518, Section " + + section + ") states that keys used with " + name() + " MUST have a size >= " + + this.minKeyLength + " bits. Consider using the " + Keys.class.getName() + " class's " + + "'keyPairFor(SignatureAlgorithm." + name() + ")' method to create a key pair guaranteed " + + "to be secure enough for " + name() + ". See " + + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; throw new WeakKeyException(msg); } } @@ -563,19 +566,19 @@ public enum SignatureAlgorithm { } if (!(key instanceof SecretKey || - (key instanceof PrivateKey && (key instanceof ECKey || key instanceof RSAKey)))) { + (key instanceof PrivateKey && (key instanceof ECKey || key instanceof RSAKey)))) { String msg = "JWT standard signing algorithms require either 1) a SecretKey for HMAC-SHA algorithms or " + - "2) a private RSAKey for RSA algorithms or 3) a private ECKey for Elliptic Curve algorithms. " + - "The specified key is of type " + key.getClass().getName(); + "2) a private RSAKey for RSA algorithms or 3) a private ECKey for Elliptic Curve algorithms. " + + "The specified key is of type " + key.getClass().getName(); throw new InvalidKeyException(msg); } if (key instanceof SecretKey) { - SecretKey secretKey = (SecretKey)key; + SecretKey secretKey = (SecretKey) key; int bitLength = io.jsonwebtoken.lang.Arrays.length(secretKey.getEncoded()) * Byte.SIZE; - for(SignatureAlgorithm alg : PREFERRED_HMAC_ALGS) { + for (SignatureAlgorithm alg : PREFERRED_HMAC_ALGS) { // ensure compatibility check is based on key length. See https://github.com/jwtk/jjwt/issues/381 if (bitLength >= alg.minKeyLength) { return alg; @@ -583,9 +586,9 @@ public enum SignatureAlgorithm { } String msg = "The specified SecretKey is not strong enough to be used with JWT HMAC signature " + - "algorithms. The JWT specification requires HMAC keys to be >= 256 bits long. The specified " + - "key is " + bitLength + " bits. See https://tools.ietf.org/html/rfc7518#section-3.2 for more " + - "information."; + "algorithms. The JWT specification requires HMAC keys to be >= 256 bits long. The specified " + + "key is " + bitLength + " bits. See https://tools.ietf.org/html/rfc7518#section-3.2 for more " + + "information."; throw new WeakKeyException(msg); } @@ -606,9 +609,9 @@ public enum SignatureAlgorithm { } String msg = "The specified RSA signing key is not strong enough to be used with JWT RSA signature " + - "algorithms. The JWT specification requires RSA keys to be >= 2048 bits long. The specified RSA " + - "key is " + bitLength + " bits. See https://tools.ietf.org/html/rfc7518#section-3.3 for more " + - "information."; + "algorithms. The JWT specification requires RSA keys to be >= 2048 bits long. The specified RSA " + + "key is " + bitLength + " bits. See https://tools.ietf.org/html/rfc7518#section-3.3 for more " + + "information."; throw new WeakKeyException(msg); } @@ -626,9 +629,9 @@ public enum SignatureAlgorithm { } String msg = "The specified Elliptic Curve signing key is not strong enough to be used with JWT ECDSA " + - "signature algorithms. The JWT specification requires ECDSA keys to be >= 256 bits long. " + - "The specified ECDSA key is " + bitLength + " bits. See " + - "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; + "signature algorithms. The JWT specification requires ECDSA keys to be >= 256 bits long. " + + "The specified ECDSA key is " + bitLength + " bits. See " + + "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; throw new WeakKeyException(msg); } diff --git a/api/src/main/java/io/jsonwebtoken/SignatureException.java b/api/src/main/java/io/jsonwebtoken/SignatureException.java index e98b4c72..7a54cda1 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureException.java @@ -21,15 +21,26 @@ import io.jsonwebtoken.security.SecurityException; * Exception indicating that either calculating a signature or verifying an existing signature of a JWT failed. * * @since 0.1 - * @deprecated in favor of {@link io.jsonwebtoken.security.SecurityException}; this class will be removed before 1.0 + * @deprecated in favor of {@link io.jsonwebtoken.security.SignatureException}; this class will be removed before 1.0 */ @Deprecated public class SignatureException extends SecurityException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public SignatureException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SignatureException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index 6cc6c01b..33f642be 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -22,7 +22,7 @@ import java.security.Key; * should be used to verify a JWS signature. * *

A {@code SigningKeyResolver} is necessary when the signing key is not already known before parsing the JWT and the - * JWT header or payload (plaintext body or Claims) must be inspected first to determine how to look up the signing key. + * JWT header or payload (byte array or Claims) must be inspected first to determine how to look up the signing key. * Once returned by the resolver, the JwtParser will then verify the JWS signature with the returned key. For * example:

* @@ -40,13 +40,15 @@ import java.security.Key; * *

Using an Adapter

* - *

If you only need to resolve a signing key for a particular JWS (either a plaintext or Claims JWS), consider using + *

If you only need to resolve a signing key for a particular JWS (either a content or Claims JWS), consider using * the {@link io.jsonwebtoken.SigningKeyResolverAdapter} and overriding only the method you need to support instead of * implementing this interface directly.

* - * @see io.jsonwebtoken.SigningKeyResolverAdapter + * @see io.jsonwebtoken.JwtParserBuilder#setKeyLocator(Locator) * @since 0.4 + * @deprecated since JJWT_RELEASE_VERSION. Implement {@link Locator} instead. */ +@Deprecated public interface SigningKeyResolver { /** @@ -54,20 +56,20 @@ public interface SigningKeyResolver { * header and claims. * * @param header the header of the JWS to validate - * @param claims the claims (body) of the JWS to validate + * @param claims the Claims payload of the JWS to validate * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified * header and claims. */ Key resolveSigningKey(JwsHeader header, Claims claims); /** - * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the - * specified header and plaintext payload. + * Returns the signing key that should be used to validate a digital signature for the content JWS with the + * specified header and byte array payload. * - * @param header the header of the JWS to validate - * @param plaintext the plaintext body of the JWS to validate - * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the - * specified header and plaintext payload. + * @param header the header of the JWS to validate + * @param content the byte array payload of the JWS to validate + * @return the signing key that should be used to validate a digital signature for the content JWS with the + * specified header and byte array payload. */ - Key resolveSigningKey(JwsHeader header, String plaintext); + Key resolveSigningKey(JwsHeader header, byte[] content); } diff --git a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index 1be7ec55..c3af881f 100644 --- a/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -21,44 +21,67 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; /** - * An Adapter implementation of the - * {@link SigningKeyResolver} interface that allows subclasses to process only the type of JWS body that - * is known/expected for a particular case. + *

Deprecation Notice

* - *

The {@link #resolveSigningKey(JwsHeader, Claims)} and {@link #resolveSigningKey(JwsHeader, String)} method + *

As of JJWT JJWT_RELEASE_VERSION, various Resolver concepts (including the {@code SigningKeyResolver}) have been + * unified into a single {@link Locator} interface. For key location, (for both signing and encryption keys), + * use the {@link JwtParserBuilder#setKeyLocator(Locator)} to configure a parser with your desired Key locator instead + * of using a {@code SigningKeyResolver}. Also see {@link LocatorAdapter} for the Adapter pattern parallel of this + * class. This {@code SigningKeyResolverAdapter} class will be removed before the 1.0 release.

+ * + *

Previous Documentation

+ * + *

An Adapter implementation of the + * {@link SigningKeyResolver} interface that allows subclasses to process only the type of JWS body that + * is known/expected for a particular case.

+ * + *

The {@link #resolveSigningKey(JwsHeader, Claims)} and {@link #resolveSigningKey(JwsHeader, byte[])} method * implementations delegate to the - * {@link #resolveSigningKeyBytes(JwsHeader, Claims)} and {@link #resolveSigningKeyBytes(JwsHeader, String)} methods + * {@link #resolveSigningKeyBytes(JwsHeader, Claims)} and {@link #resolveSigningKeyBytes(JwsHeader, byte[])} methods * respectively. The latter two methods simply throw exceptions: they represent scenarios expected by * calling code in known situations, and it is expected that you override the implementation in those known situations; * non-overridden *KeyBytes methods indicates that the JWS input was unexpected.

* - *

If either {@link #resolveSigningKey(JwsHeader, String)} or {@link #resolveSigningKey(JwsHeader, Claims)} + *

If either {@link #resolveSigningKey(JwsHeader, byte[])} or {@link #resolveSigningKey(JwsHeader, Claims)} * are not overridden, one (or both) of the *KeyBytes variants must be overridden depending on your expected * use case. You do not have to override any method that does not represent an expected condition.

* + * @see io.jsonwebtoken.JwtParserBuilder#setKeyLocator(Locator) + * @see LocatorAdapter * @since 0.4 + * @deprecated since JJWT_RELEASE_VERSION. Use {@link LocatorAdapter LocatorAdapter} with + * {@link JwtParserBuilder#setKeyLocator(Locator)} */ +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated public class SigningKeyResolverAdapter implements SigningKeyResolver { + /** + * Default constructor. + */ + public SigningKeyResolverAdapter() { + + } + @Override public Key resolveSigningKey(JwsHeader header, Claims claims) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot " + + "be used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); byte[] keyBytes = resolveSigningKeyBytes(header, claims); return new SecretKeySpec(keyBytes, alg.getJcaName()); } @Override - public Key resolveSigningKey(JwsHeader header, String plaintext) { + public Key resolveSigningKey(JwsHeader header, byte[] content) { SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm()); - Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " + - "used for asymmetric key algorithms (RSA, Elliptic Curve). " + - "Override the resolveSigningKey(JwsHeader, String) method instead and return a " + - "Key instance appropriate for the " + alg.name() + " algorithm."); - byte[] keyBytes = resolveSigningKeyBytes(header, plaintext); + Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, byte[]) implementation cannot " + + "be used for asymmetric key algorithms (RSA, Elliptic Curve). " + + "Override the resolveSigningKey(JwsHeader, byte[]) method instead and return a " + + "Key instance appropriate for the " + alg.name() + " algorithm."); + byte[] keyBytes = resolveSigningKeyBytes(header, content); return new SecretKeySpec(keyBytes, alg.getJcaName()); } @@ -76,24 +99,25 @@ public class SigningKeyResolverAdapter implements SigningKeyResolver { */ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "Claims JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, Claims) method."); + "Claims JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, Claims) method."); } /** - * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing - * key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must - * override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead. + * Convenience method invoked by {@link #resolveSigningKey(JwsHeader, byte[])} that obtains the necessary signing + * key bytes. This implementation simply throws an exception: if the JWS parsed is a content JWS, you must + * override this method or the {@link #resolveSigningKey(JwsHeader, byte[])} method instead. * - * @param header the parsed {@link JwsHeader} - * @param payload the parsed String plaintext payload + * @param header the parsed {@link JwsHeader} + * @param content the byte array payload * @return the signing key bytes to use to verify the JWS signature. */ - public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + @SuppressWarnings("unused") + public byte[] resolveSigningKeyBytes(JwsHeader header, byte[] content) { throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " + - "plaintext JWS signing key resolution. Consider overriding either the " + - "resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " + - "resolveSigningKeyBytes(JwsHeader, String) method."); + "content JWS signing key resolution. Consider overriding either the " + + "resolveSigningKey(JwsHeader, byte[]) method or, for HMAC algorithms, the " + + "resolveSigningKeyBytes(JwsHeader, byte[]) method."); } } diff --git a/api/src/main/java/io/jsonwebtoken/UnprotectedHeader.java b/api/src/main/java/io/jsonwebtoken/UnprotectedHeader.java new file mode 100644 index 00000000..3aef82c8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/UnprotectedHeader.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * A JWT {@link Header} that is not integrity protected via either digital signature or encryption. It will + * always have an {@link #getAlgorithm() algorithm} of {@code none}. + * + * @since JJWT_RELEASE_VERSION + */ +public interface UnprotectedHeader extends Header { +} diff --git a/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java b/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java index 3735f7d4..a1ec9680 100644 --- a/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java +++ b/api/src/main/java/io/jsonwebtoken/UnsupportedJwtException.java @@ -19,17 +19,28 @@ package io.jsonwebtoken; * Exception thrown when receiving a JWT in a particular format/configuration that does not match the format expected * by the application. * - *

For example, this exception would be thrown if parsing an unsigned plaintext JWT when the application + *

For example, this exception would be thrown if parsing an unprotected content JWT when the application * requires a cryptographically signed Claims JWS instead.

* * @since 0.2 */ public class UnsupportedJwtException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public UnsupportedJwtException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public UnsupportedJwtException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java index 3c8bc817..fa76d3aa 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64Decoder.java @@ -18,6 +18,9 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.lang.Assert; /** + * Very fast Base64 decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64Decoder extends Base64Support implements Decoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java index 963d8810..cefad2f9 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64Encoder.java @@ -18,6 +18,9 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.lang.Assert; /** + * Very fast Base64 encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64Encoder extends Base64Support implements Encoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64Support.java b/api/src/main/java/io/jsonwebtoken/io/Base64Support.java index eed404d5..8f8a4c13 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64Support.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64Support.java @@ -18,6 +18,8 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.lang.Assert; /** + * Parent class for Base64 encoders and decoders. + * * @since 0.10.0 */ class Base64Support { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java index 0d26d948..fcca4cba 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64UrlDecoder.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.io; /** + * Very fast Base64Url decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64UrlDecoder extends Base64Decoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java b/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java index 86781922..1377d31e 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Base64UrlEncoder.java @@ -16,6 +16,9 @@ package io.jsonwebtoken.io; /** + * Very fast Base64Url encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + * * @since 0.10.0 */ class Base64UrlEncoder extends Base64Encoder { diff --git a/api/src/main/java/io/jsonwebtoken/io/CodecException.java b/api/src/main/java/io/jsonwebtoken/io/CodecException.java index 5b449bd9..f25d8ca0 100644 --- a/api/src/main/java/io/jsonwebtoken/io/CodecException.java +++ b/api/src/main/java/io/jsonwebtoken/io/CodecException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * An exception thrown when encountering a problem during encoding or decoding. + * * @since 0.10.0 */ public class CodecException extends IOException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public CodecException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public CodecException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Decoder.java b/api/src/main/java/io/jsonwebtoken/io/Decoder.java index f9f2c189..da02e805 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Decoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Decoder.java @@ -16,9 +16,18 @@ package io.jsonwebtoken.io; /** + * A decoder converts an already-encoded data value to a desired data type. + * * @since 0.10.0 */ public interface Decoder { + /** + * Convert the specified encoded data value into the desired data type. + * + * @param t the encoded data + * @return the resulting expected data + * @throws DecodingException if there is a problem during decoding. + */ R decode(T t) throws DecodingException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Decoders.java b/api/src/main/java/io/jsonwebtoken/io/Decoders.java index 3e95c28d..57a14149 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Decoders.java +++ b/api/src/main/java/io/jsonwebtoken/io/Decoders.java @@ -16,11 +16,24 @@ package io.jsonwebtoken.io; /** + * Constant definitions for various decoding algorithms. + * + * @see #BASE64 + * @see #BASE64URL * @since 0.10.0 */ public final class Decoders { + /** + * Very fast Base64 decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Decoder BASE64 = new ExceptionPropagatingDecoder<>(new Base64Decoder()); + + /** + * Very fast Base64Url decoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Decoder BASE64URL = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder()); private Decoders() { //prevent instantiation diff --git a/api/src/main/java/io/jsonwebtoken/io/DecodingException.java b/api/src/main/java/io/jsonwebtoken/io/DecodingException.java index 0bbe62a1..ab3df921 100644 --- a/api/src/main/java/io/jsonwebtoken/io/DecodingException.java +++ b/api/src/main/java/io/jsonwebtoken/io/DecodingException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * An exception thrown when encountering a problem during decoding. + * * @since 0.10.0 */ public class DecodingException extends CodecException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public DecodingException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public DecodingException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java b/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java index 59559666..76c647ce 100644 --- a/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java +++ b/api/src/main/java/io/jsonwebtoken/io/DeserializationException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * Exception thrown when reconstituting a serialized byte array into a Java object. + * * @since 0.10.0 */ public class DeserializationException extends SerialException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public DeserializationException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public DeserializationException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java index f66a73ed..a83b3dc6 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java @@ -16,9 +16,19 @@ package io.jsonwebtoken.io; /** + * A {@code Deserializer} is able to convert serialized data byte arrays into Java objects. + * + * @param the type of object to be returned as a result of deserialization. * @since 0.10.0 */ public interface Deserializer { + /** + * Convert the specified formatted data byte array into a Java object. + * + * @param bytes the formatted data byte array to convert + * @return the reconstituted Java object + * @throws DeserializationException if there is a problem converting the byte array to to an object. + */ T deserialize(byte[] bytes) throws DeserializationException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Encoder.java b/api/src/main/java/io/jsonwebtoken/io/Encoder.java index 6b28f31e..f334ee8c 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Encoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/Encoder.java @@ -16,9 +16,20 @@ package io.jsonwebtoken.io; /** + * An encoder converts data of one type into another formatted data value. + * + * @param the type of data to convert + * @param the type of the resulting formatted data * @since 0.10.0 */ public interface Encoder { + /** + * Convert the specified data into another formatted data value. + * + * @param t the data to convert + * @return the resulting formatted data value + * @throws EncodingException if there is a problem during encoding + */ R encode(T t) throws EncodingException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Encoders.java b/api/src/main/java/io/jsonwebtoken/io/Encoders.java index 3b7c060f..17f03f25 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Encoders.java +++ b/api/src/main/java/io/jsonwebtoken/io/Encoders.java @@ -16,11 +16,24 @@ package io.jsonwebtoken.io; /** + * Constant definitions for various encoding algorithms. + * + * @see #BASE64 + * @see #BASE64URL * @since 0.10.0 */ public final class Encoders { + /** + * Very fast Base64 encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Encoder BASE64 = new ExceptionPropagatingEncoder<>(new Base64Encoder()); + + /** + * Very fast Base64Url encoder guaranteed to + * work in all >= Java 7 JDK and Android environments. + */ public static final Encoder BASE64URL = new ExceptionPropagatingEncoder<>(new Base64UrlEncoder()); private Encoders() { //prevent instantiation diff --git a/api/src/main/java/io/jsonwebtoken/io/EncodingException.java b/api/src/main/java/io/jsonwebtoken/io/EncodingException.java index 5b65389a..c5ee9f90 100644 --- a/api/src/main/java/io/jsonwebtoken/io/EncodingException.java +++ b/api/src/main/java/io/jsonwebtoken/io/EncodingException.java @@ -16,10 +16,18 @@ package io.jsonwebtoken.io; /** + * An exception thrown when encountering a problem during encoding. + * * @since 0.10.0 */ public class EncodingException extends CodecException { + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public EncodingException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java index a0d98134..9e5bc78e 100644 --- a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingDecoder.java @@ -18,17 +18,33 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.lang.Assert; /** + * Decoder that ensures any exceptions thrown that are not {@link DecodingException}s are wrapped + * and re-thrown as a {@code DecodingException}. + * * @since 0.10.0 */ class ExceptionPropagatingDecoder implements Decoder { private final Decoder decoder; + /** + * Creates a new instance, wrapping the specified {@code decoder} to invoke during {@link #decode(Object)}. + * + * @param decoder the decoder to wrap and call during {@link #decode(Object)} + */ ExceptionPropagatingDecoder(Decoder decoder) { Assert.notNull(decoder, "Decoder cannot be null."); this.decoder = decoder; } + /** + * Decode the specified encoded data, delegating to the wrapped Decoder, wrapping any + * non-{@link DecodingException} as a {@code DecodingException}. + * + * @param t the encoded data + * @return the decoded data + * @throws DecodingException if there is an unexpected problem during decoding. + */ @Override public R decode(T t) throws DecodingException { Assert.notNull(t, "Decode argument cannot be null."); diff --git a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java index c483c8cd..8efca957 100644 --- a/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java +++ b/api/src/main/java/io/jsonwebtoken/io/ExceptionPropagatingEncoder.java @@ -18,17 +18,33 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.lang.Assert; /** + * Encoder that ensures any exceptions thrown that are not {@link EncodingException}s are wrapped + * and re-thrown as a {@code EncodingException}. + * * @since 0.10.0 */ class ExceptionPropagatingEncoder implements Encoder { private final Encoder encoder; + /** + * Creates a new instance, wrapping the specified {@code encoder} to invoke during {@link #encode(Object)}. + * + * @param encoder the encoder to wrap and call during {@link #encode(Object)} + */ ExceptionPropagatingEncoder(Encoder encoder) { Assert.notNull(encoder, "Encoder cannot be null."); this.encoder = encoder; } + /** + * Encoded the specified data, delegating to the wrapped Encoder, wrapping any + * non-{@link EncodingException} as an {@code EncodingException}. + * + * @param t the data to encode + * @return the encoded data + * @throws EncodingException if there is an unexpected problem during encoding. + */ @Override public R encode(T t) throws EncodingException { Assert.notNull(t, "Encode argument cannot be null."); diff --git a/api/src/main/java/io/jsonwebtoken/io/IOException.java b/api/src/main/java/io/jsonwebtoken/io/IOException.java index 9ed1e031..0ccd165c 100644 --- a/api/src/main/java/io/jsonwebtoken/io/IOException.java +++ b/api/src/main/java/io/jsonwebtoken/io/IOException.java @@ -18,14 +18,28 @@ package io.jsonwebtoken.io; import io.jsonwebtoken.JwtException; /** + * JJWT's base exception for problems during data input or output operations, such as serialization, + * deserialization, marshalling, unmarshalling, etc. + * * @since 0.10.0 */ public class IOException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public IOException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public IOException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/SerialException.java b/api/src/main/java/io/jsonwebtoken/io/SerialException.java index 86f70920..0269c96e 100644 --- a/api/src/main/java/io/jsonwebtoken/io/SerialException.java +++ b/api/src/main/java/io/jsonwebtoken/io/SerialException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * An exception thrown during serialization or deserialization. + * * @since 0.10.0 */ public class SerialException extends IOException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public SerialException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SerialException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/SerializationException.java b/api/src/main/java/io/jsonwebtoken/io/SerializationException.java index 514830de..d9789211 100644 --- a/api/src/main/java/io/jsonwebtoken/io/SerializationException.java +++ b/api/src/main/java/io/jsonwebtoken/io/SerializationException.java @@ -16,14 +16,27 @@ package io.jsonwebtoken.io; /** + * Exception thrown when converting a Java object to a formatted byte array. + * * @since 0.10.0 */ public class SerializationException extends SerialException { + /** + * Creates a new instance with the specified explanation message. + * + * @param msg the message explaining why the exception is thrown. + */ public SerializationException(String msg) { super(msg); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SerializationException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/io/Serializer.java b/api/src/main/java/io/jsonwebtoken/io/Serializer.java index 5d6cd794..630cfe89 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Serializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Serializer.java @@ -16,10 +16,21 @@ package io.jsonwebtoken.io; /** + * A {@code Serializer} is able to convert a Java object into a formatted data byte array. It is expected this data + * can be reconstituted back into a Java object with a matching {@link Deserializer}. + * + * @param The type of object to serialize. * @since 0.10.0 */ public interface Serializer { + /** + * Convert the specified Java object into a formatted data byte array. + * + * @param t the object to serialize + * @return the serialized byte array representing the specified object. + * @throws SerializationException if there is a problem converting the object to a byte array. + */ byte[] serialize(T t) throws SerializationException; } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java index 6b58a376..6c5b4e2a 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Arrays.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Arrays.java @@ -15,18 +15,105 @@ */ package io.jsonwebtoken.lang; +import java.lang.reflect.Array; +import java.util.List; + /** + * Utility methods to work with array instances. + * * @since 0.6 */ public final class Arrays { - private Arrays(){} //prevent instantiation + private Arrays() { + } //prevent instantiation + /** + * Returns the length of the array, or {@code 0} if the array is {@code null}. + * + * @param a the possibly-null array + * @param the type of elements in the array + * @return the length of the array, or zero if the array is null. + */ + public static int length(T[] a) { + return a == null ? 0 : a.length; + } + + /** + * Converts the specified array to a {@link List}. If the array is empty, an empty list will be returned. + * + * @param a the array to represent as a list + * @param the type of elements in the array + * @return the array as a list, or an empty list if the array is empty. + */ + public static List asList(T[] a) { + return Objects.isEmpty(a) ? Collections.emptyList() : java.util.Arrays.asList(a); + } + + /** + * Returns the length of the specified byte array, or {@code 0} if the byte array is {@code null}. + * + * @param bytes the array to check + * @return the length of the specified byte array, or {@code 0} if the byte array is {@code null}. + */ public static int length(byte[] bytes) { return bytes != null ? bytes.length : 0; } + /** + * Returns the byte array unaltered if it is non-null and has a positive length, otherwise {@code null}. + * + * @param bytes the byte array to check. + * @return the byte array unaltered if it is non-null and has a positive length, otherwise {@code null}. + */ public static byte[] clean(byte[] bytes) { return length(bytes) > 0 ? bytes : null; } + + /** + * Creates a shallow copy of the specified object or array. + * + * @param obj the object to copy + * @return a shallow copy of the specified object or array. + */ + public static Object copy(Object obj) { + if (obj == null) { + return null; + } + Assert.isTrue(Objects.isArray(obj), "Argument must be an array."); + if (obj instanceof Object[]) { + return ((Object[]) obj).clone(); + } + if (obj instanceof boolean[]) { + return ((boolean[]) obj).clone(); + } + if (obj instanceof byte[]) { + return ((byte[]) obj).clone(); + } + if (obj instanceof char[]) { + return ((char[]) obj).clone(); + } + if (obj instanceof double[]) { + return ((double[]) obj).clone(); + } + if (obj instanceof float[]) { + return ((float[]) obj).clone(); + } + if (obj instanceof int[]) { + return ((int[]) obj).clone(); + } + if (obj instanceof long[]) { + return ((long[]) obj).clone(); + } + if (obj instanceof short[]) { + return ((short[]) obj).clone(); + } + Class componentType = obj.getClass().getComponentType(); + int length = Array.getLength(obj); + Object[] copy = (Object[]) Array.newInstance(componentType, length); + for (int i = 0; i < length; i++) { + copy[i] = Array.get(obj, i); + } + return copy; + } } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Assert.java b/api/src/main/java/io/jsonwebtoken/lang/Assert.java index 7c9e7c36..2216aa77 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Assert.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -18,16 +18,22 @@ package io.jsonwebtoken.lang; import java.util.Collection; import java.util.Map; +/** + * Utility methods for providing argument and state assertions to reduce repeating these patterns and otherwise + * increasing cyclomatic complexity. + */ public final class Assert { - private Assert(){} //prevent instantiation + private Assert() { + } //prevent instantiation /** * Assert a boolean expression, throwing IllegalArgumentException * if the test result is false. *
Assert.isTrue(i > 0, "The value must be greater than zero");
+ * * @param expression a boolean expression - * @param message the exception message to use if the assertion fails + * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if expression is false */ public static void isTrue(boolean expression, String message) { @@ -40,6 +46,7 @@ public final class Assert { * Assert a boolean expression, throwing IllegalArgumentException * if the test result is false. *
Assert.isTrue(i > 0);
+ * * @param expression a boolean expression * @throws IllegalArgumentException if expression is false */ @@ -50,7 +57,8 @@ public final class Assert { /** * Assert that an object is null . *
Assert.isNull(value, "The value must be null");
- * @param object the object to check + * + * @param object the object to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is not null */ @@ -63,6 +71,7 @@ public final class Assert { /** * Assert that an object is null . *
Assert.isNull(value);
+ * * @param object the object to check * @throws IllegalArgumentException if the object is not null */ @@ -73,19 +82,24 @@ public final class Assert { /** * Assert that an object is not null . *
Assert.notNull(clazz, "The class must not be null");
- * @param object the object to check + * + * @param object the object to check + * @param the type of object * @param message the exception message to use if the assertion fails + * @return the non-null object * @throws IllegalArgumentException if the object is null */ - public static void notNull(Object object, String message) { + public static T notNull(T object, String message) { if (object == null) { throw new IllegalArgumentException(message); } + return object; } /** * Assert that an object is not null . *
Assert.notNull(clazz);
+ * * @param object the object to check * @throws IllegalArgumentException if the object is null */ @@ -97,7 +111,8 @@ public final class Assert { * Assert that the given String is not empty; that is, * it must not be null and not the empty String. *
Assert.hasLength(name, "Name must not be empty");
- * @param text the String to check + * + * @param text the String to check * @param message the exception message to use if the assertion fails * @see Strings#hasLength */ @@ -111,32 +126,37 @@ public final class Assert { * Assert that the given String is not empty; that is, * it must not be null and not the empty String. *
Assert.hasLength(name);
+ * * @param text the String to check * @see Strings#hasLength */ public static void hasLength(String text) { hasLength(text, - "[Assertion failed] - this String argument must have length; it must not be null or empty"); + "[Assertion failed] - this String argument must have length; it must not be null or empty"); } /** * Assert that the given String has valid text content; that is, it must not * be null and must contain at least one non-whitespace character. *
Assert.hasText(name, "'name' must not be empty");
- * @param text the String to check + * + * @param text the String to check * @param message the exception message to use if the assertion fails + * @return the string if it has text * @see Strings#hasText */ - public static void hasText(String text, String message) { + public static String hasText(String text, String message) { if (!Strings.hasText(text)) { throw new IllegalArgumentException(message); } + return text; } /** * Assert that the given String has valid text content; that is, it must not * be null and must contain at least one non-whitespace character. *
Assert.hasText(name, "'name' must not be empty");
+ * * @param text the String to check * @see Strings#hasText */ @@ -148,13 +168,14 @@ public final class Assert { /** * Assert that the given text does not contain the given substring. *
Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * * @param textToSearch the text to search - * @param substring the substring to find within the text - * @param message the exception message to use if the assertion fails + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails */ public static void doesNotContain(String textToSearch, String substring, String message) { if (Strings.hasLength(textToSearch) && Strings.hasLength(substring) && - textToSearch.indexOf(substring) != -1) { + textToSearch.indexOf(substring) != -1) { throw new IllegalArgumentException(message); } } @@ -162,12 +183,13 @@ public final class Assert { /** * Assert that the given text does not contain the given substring. *
Assert.doesNotContain(name, "rod");
+ * * @param textToSearch the text to search - * @param substring the substring to find within the text + * @param substring the substring to find within the text */ public static void doesNotContain(String textToSearch, String substring) { doesNotContain(textToSearch, substring, - "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); } @@ -175,20 +197,24 @@ public final class Assert { * Assert that an array has elements; that is, it must not be * null and must have at least one element. *
Assert.notEmpty(array, "The array must have elements");
- * @param array the array to check + * + * @param array the array to check * @param message the exception message to use if the assertion fails + * @return the non-empty array for immediate use * @throws IllegalArgumentException if the object array is null or has no elements */ - public static void notEmpty(Object[] array, String message) { + public static Object[] notEmpty(Object[] array, String message) { if (Objects.isEmpty(array)) { throw new IllegalArgumentException(message); } + return array; } /** * Assert that an array has elements; that is, it must not be * null and must have at least one element. *
Assert.notEmpty(array);
+ * * @param array the array to check * @throws IllegalArgumentException if the object array is null or has no elements */ @@ -196,17 +222,44 @@ public final class Assert { notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); } - public static void notEmpty(byte[] array, String msg) { + /** + * Assert that the specified byte array is not null and has at least one byte element. + * + * @param array the byte array to check + * @param msg the exception message to use if the assertion fails + * @return the byte array if the assertion passes + * @throws IllegalArgumentException if the byte array is null or empty + * @since JJWT_RELEASE_VERSION + */ + public static byte[] notEmpty(byte[] array, String msg) { if (Objects.isEmpty(array)) { throw new IllegalArgumentException(msg); } + return array; + } + + /** + * Assert that the specified character array is not null and has at least one byte element. + * + * @param chars the character array to check + * @param msg the exception message to use if the assertion fails + * @return the character array if the assertion passes + * @throws IllegalArgumentException if the character array is null or empty + * @since JJWT_RELEASE_VERSION + */ + public static char[] notEmpty(char[] chars, String msg) { + if (Objects.isEmpty(chars)) { + throw new IllegalArgumentException(msg); + } + return chars; } /** * Assert that an array has no null elements. * Note: Does not complain if the array is empty! *
Assert.noNullElements(array, "The array must have non-null elements");
- * @param array the array to check + * + * @param array the array to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object array contains a null element */ @@ -224,6 +277,7 @@ public final class Assert { * Assert that an array has no null elements. * Note: Does not complain if the array is empty! *
Assert.noNullElements(array);
+ * * @param array the array to check * @throws IllegalArgumentException if the object array contains a null element */ @@ -235,46 +289,56 @@ public final class Assert { * Assert that a collection has elements; that is, it must not be * null and must have at least one element. *
Assert.notEmpty(collection, "Collection must have elements");
+ * * @param collection the collection to check - * @param message the exception message to use if the assertion fails + * @param the type of collection + * @param message the exception message to use if the assertion fails + * @return the non-null, non-empty collection * @throws IllegalArgumentException if the collection is null or has no elements */ - public static void notEmpty(Collection collection, String message) { + public static > T notEmpty(T collection, String message) { if (Collections.isEmpty(collection)) { throw new IllegalArgumentException(message); } + return collection; } /** * Assert that a collection has elements; that is, it must not be * null and must have at least one element. *
Assert.notEmpty(collection, "Collection must have elements");
+ * * @param collection the collection to check * @throws IllegalArgumentException if the collection is null or has no elements */ - public static void notEmpty(Collection collection) { + public static void notEmpty(Collection collection) { notEmpty(collection, - "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); } /** * Assert that a Map has entries; that is, it must not be null * and must have at least one entry. *
Assert.notEmpty(map, "Map must have entries");
- * @param map the map to check + * + * @param map the map to check + * @param the type of Map to check * @param message the exception message to use if the assertion fails + * @return the non-null, non-empty map * @throws IllegalArgumentException if the map is null or has no entries */ - public static void notEmpty(Map map, String message) { + public static > T notEmpty(T map, String message) { if (Collections.isEmpty(map)) { throw new IllegalArgumentException(message); } + return map; } /** * Assert that a Map has entries; that is, it must not be null * and must have at least one entry. *
Assert.notEmpty(map);
+ * * @param map the map to check * @throws IllegalArgumentException if the map is null or has no entries */ @@ -286,41 +350,49 @@ public final class Assert { /** * Assert that the provided object is an instance of the provided class. *
Assert.instanceOf(Foo.class, foo);
+ * + * @param the type of instance expected * @param clazz the required class - * @param obj the object to check + * @param obj the object to check + * @return the expected instance of type {@code T} * @throws IllegalArgumentException if the object is not an instance of clazz * @see Class#isInstance */ - public static void isInstanceOf(Class clazz, Object obj) { - isInstanceOf(clazz, obj, ""); + public static T isInstanceOf(Class clazz, Object obj) { + return isInstanceOf(clazz, obj, ""); } /** * Assert that the provided object is an instance of the provided class. *
Assert.instanceOf(Foo.class, foo);
- * @param type the type to check against - * @param obj the object to check + * + * @param type the type to check against + * @param the object's expected type + * @param obj the object to check * @param message a message which will be prepended to the message produced by - * the function itself, and which may be used to provide context. It should - * normally end in a ": " or ". " so that the function generate message looks - * ok when prepended to it. + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @return the non-null object IFF it is an instance of the specified {@code type}. * @throws IllegalArgumentException if the object is not an instance of clazz * @see Class#isInstance */ - public static void isInstanceOf(Class type, Object obj, String message) { + public static T isInstanceOf(Class type, Object obj, String message) { notNull(type, "Type to check against must not be null"); if (!type.isInstance(obj)) { throw new IllegalArgumentException(message + - "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + - "] must be an instance of " + type); + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); } + return type.cast(obj); } /** * Assert that superType.isAssignableFrom(subType) is true. *
Assert.isAssignable(Number.class, myClass);
+ * * @param superType the super type to check - * @param subType the sub type to check + * @param subType the sub type to check * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, Class subType) { @@ -330,12 +402,13 @@ public final class Assert { /** * Assert that superType.isAssignableFrom(subType) is true. *
Assert.isAssignable(Number.class, myClass);
+ * * @param superType the super type to check against - * @param subType the sub type to check - * @param message a message which will be prepended to the message produced by - * the function itself, and which may be used to provide context. It should - * normally end in a ": " or ". " so that the function generate message looks - * ok when prepended to it. + * @param subType the sub type to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, Class subType, String message) { @@ -345,14 +418,54 @@ public final class Assert { } } + /** + * Asserts that a specified {@code value} is equal to the given {@code requirement}, throwing + * an {@link IllegalArgumentException} with the given message if not. + * + * @param the type of argument + * @param requirement the integer that {@code value} must be greater than + * @param value the value to check + * @param msg the message to use for the {@code IllegalArgumentException} if thrown. + * @return {@code value} if greater than the specified {@code requirement}. + * @since JJWT_RELEASE_VERSION + */ + public static T eq(T requirement, T value, String msg) { + notNull(requirement, "requirement cannot be null."); + notNull(value, "value cannot be null."); + if (!requirement.equals(value)) { + throw new IllegalArgumentException(msg); + } + return value; + } + + /** + * Asserts that a specified {@code value} is greater than the given {@code requirement}, throwing + * an {@link IllegalArgumentException} with the given message if not. + * + * @param value the value to check + * @param requirement the integer that {@code value} must be greater than + * @param msg the message to use for the {@code IllegalArgumentException} if thrown. + * @return {@code value} if greater than the specified {@code requirement}. + * @since JJWT_RELEASE_VERSION + */ + public static Integer gt(Integer value, Integer requirement, String msg) { + notNull(value, "value cannot be null."); + notNull(requirement, "requirement cannot be null."); + if (!(value > requirement)) { + throw new IllegalArgumentException(msg); + } + return value; + } + /** * Assert a boolean expression, throwing IllegalStateException * if the test result is false. Call isTrue if you wish to * throw IllegalArgumentException on an assertion failure. *
Assert.state(id == null, "The id property must not already be initialized");
+ * * @param expression a boolean expression - * @param message the exception message to use if the assertion fails + * @param message the exception message to use if the assertion fails * @throws IllegalStateException if expression is false */ public static void state(boolean expression, String message) { @@ -367,6 +480,7 @@ public final class Assert { *

Call {@link #isTrue(boolean)} if you wish to * throw {@link IllegalArgumentException} on an assertion failure. *

Assert.state(id == null);
+ * * @param expression a boolean expression * @throws IllegalStateException if the supplied expression is false */ @@ -374,4 +488,21 @@ public final class Assert { state(expression, "[Assertion failed] - this state invariant must be true"); } + /** + * Asserts that the specified {@code value} is not null, otherwise throws an + * {@link IllegalStateException} with the specified {@code msg}. Intended to be used with + * code invariants (as opposed to method arguments, like {@link #notNull(Object)}). + * + * @param value value to assert is not null + * @param msg exception message to use if {@code value} is null + * @throws IllegalStateException with the specified {@code msg} if {@code value} is null. + * @since JJWT_RELEASE_VERSION + */ + public static T stateNotNull(T value, String msg) throws IllegalStateException { + if (value == null) { + throw new IllegalStateException(msg); + } + return value; + } + } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Builder.java b/api/src/main/java/io/jsonwebtoken/lang/Builder.java new file mode 100644 index 00000000..b3a239e0 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Builder.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 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.lang; + +/** + * Type-safe interface that reflects the Builder pattern. + * + * @param The type of object that will be created each time {@link #build()} is invoked. + * @since JJWT_RELEASE_VERSION + */ +public interface Builder { + + /** + * Creates and returns a new instance of type {@code T}. + * + * @return a new instance of type {@code T}. + */ + T build(); +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/Classes.java b/api/src/main/java/io/jsonwebtoken/lang/Classes.java index 1eb6ec17..6ceca679 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -17,41 +17,36 @@ package io.jsonwebtoken.lang; import java.io.InputStream; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** + * Utility methods for working with {@link Class}es. + * * @since 0.1 */ public final class Classes { - private Classes() {} //prevent instantiation + private Classes() { + } //prevent instantiation - /** - * @since 0.1 - */ private static final ClassLoaderAccessor THREAD_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override - protected ClassLoader doGetClassLoader() throws Throwable { + protected ClassLoader doGetClassLoader() { return Thread.currentThread().getContextClassLoader(); } }; - /** - * @since 0.1 - */ private static final ClassLoaderAccessor CLASS_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override - protected ClassLoader doGetClassLoader() throws Throwable { + protected ClassLoader doGetClassLoader() { return Classes.class.getClassLoader(); } }; - /** - * @since 0.1 - */ private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() { @Override - protected ClassLoader doGetClassLoader() throws Throwable { + protected ClassLoader doGetClassLoader() { return ClassLoader.getSystemClassLoader(); } }; @@ -65,14 +60,14 @@ public final class Classes { * the JRE's ClassNotFoundException. * * @param fqcn the fully qualified class name to load - * @param The type of Class returned + * @param The type of Class returned * @return the located class * @throws UnknownClassException if the class cannot be found. */ @SuppressWarnings("unchecked") public static Class forName(String fqcn) throws UnknownClassException { - Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); + Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null) { clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); @@ -93,7 +88,7 @@ public final class Classes { throw new UnknownClassException(msg); } - return clazz; + return (Class) clazz; } /** @@ -123,6 +118,14 @@ public final class Classes { return is; } + /** + * Returns {@code true} if the specified {@code fullyQualifiedClassName} can be found in any of the thread + * context, class, or system classloaders, or {@code false} otherwise. + * + * @param fullyQualifiedClassName the fully qualified class name to check + * @return {@code true} if the specified {@code fullyQualifiedClassName} can be found in any of the thread + * context, class, or system classloaders, or {@code false} otherwise. + */ public static boolean isAvailable(String fullyQualifiedClassName) { try { forName(fullyQualifiedClassName); @@ -132,22 +135,56 @@ public final class Classes { } } + /** + * Creates and returns a new instance of the class with the specified fully qualified class name using the + * classes default no-argument constructor. + * + * @param fqcn the fully qualified class name + * @param the type of object created + * @return a new instance of the specified class name + */ @SuppressWarnings("unchecked") public static T newInstance(String fqcn) { - return (T)newInstance(forName(fqcn)); + return (T) newInstance(forName(fqcn)); } - public static T newInstance(String fqcn, Class[] ctorArgTypes, Object... args) { + /** + * Creates and returns a new instance of the specified fully qualified class name using the + * specified {@code args} arguments provided to the constructor with {@code ctorArgTypes} + * + * @param fqcn the fully qualified class name + * @param ctorArgTypes the argument types of the constructor to invoke + * @param args the arguments to supply when invoking the constructor + * @param the type of object created + * @return the newly created object + */ + public static T newInstance(String fqcn, Class[] ctorArgTypes, Object... args) { Class clazz = forName(fqcn); Constructor ctor = getConstructor(clazz, ctorArgTypes); return instantiate(ctor, args); } + /** + * Creates and returns a new instance of the specified fully qualified class name using a constructor that matches + * the specified {@code args} arguments. + * + * @param fqcn fully qualified class name + * @param args the arguments to supply to the constructor + * @param the type of the object created + * @return the newly created object + */ @SuppressWarnings("unchecked") public static T newInstance(String fqcn, Object... args) { - return (T)newInstance(forName(fqcn), args); + return (T) newInstance(forName(fqcn), args); } + /** + * Creates a new instance of the specified {@code clazz} via {@code clazz.newInstance()}. + * + * @param clazz the class to invoke + * @param the type of the object created + * @return the newly created object + */ public static T newInstance(Class clazz) { if (clazz == null) { String msg = "Class method parameter cannot be null."; @@ -160,8 +197,17 @@ public final class Classes { } } + /** + * Returns a new instance of the specified {@code clazz}, invoking the associated constructor with the specified + * {@code args} arguments. + * + * @param clazz the class to invoke + * @param args the arguments matching an associated class constructor + * @param the type of the created object + * @return the newly created object + */ public static T newInstance(Class clazz, Object... args) { - Class[] argTypes = new Class[args.length]; + Class[] argTypes = new Class[args.length]; for (int i = 0; i < args.length; i++) { argTypes[i] = args[i].getClass(); } @@ -169,7 +215,17 @@ public final class Classes { return instantiate(ctor, args); } - public static Constructor getConstructor(Class clazz, Class... argTypes) { + /** + * Returns the {@link Constructor} for the specified {@code Class} with arguments matching the specified + * {@code argTypes}. + * + * @param clazz the class to inspect + * @param argTypes the argument types for the desired constructor + * @param the type of object to create + * @return the constructor matching the specified argument types + * @throws IllegalStateException if the constructor for the specified {@code argTypes} does not exist. + */ + public static Constructor getConstructor(Class clazz, Class... argTypes) throws IllegalStateException { try { return clazz.getConstructor(argTypes); } catch (NoSuchMethodException e) { @@ -178,6 +234,16 @@ public final class Classes { } + /** + * Creates a new object using the specified {@link Constructor}, invoking it with the specified constructor + * {@code args} arguments. + * + * @param ctor the constructor to invoke + * @param args the arguments to supply to the constructor + * @param the type of object to create + * @return the new object instance + * @throws InstantiationException if the constructor cannot be invoked successfully + */ public static T instantiate(Constructor ctor, Object... args) { try { return ctor.newInstance(args); @@ -190,24 +256,51 @@ public final class Classes { /** * Invokes the fully qualified class name's method named {@code methodName} with parameters of type {@code argTypes} * using the {@code args} as the method arguments. - * @param fqcn fully qualified class name to locate + * + * @param fqcn fully qualified class name to locate * @param methodName name of the method to invoke on the class - * @param argTypes the method argument types supported by the {@code methodName} method - * @param args the runtime arguments to use when invoking the located class method - * @param the expected type of the object returned from the invoked method. + * @param argTypes the method argument types supported by the {@code methodName} method + * @param args the runtime arguments to use when invoking the located class method + * @param the expected type of the object returned from the invoked method. * @return the result returned by the invoked method * @since 0.10.0 */ - @SuppressWarnings("unchecked") - public static T invokeStatic(String fqcn, String methodName, Class[] argTypes, Object... args) { + public static T invokeStatic(String fqcn, String methodName, Class[] argTypes, Object... args) { try { - Class clazz = Classes.forName(fqcn); - Method method = clazz.getDeclaredMethod(methodName, argTypes); - method.setAccessible(true); - return(T)method.invoke(null, args); + Class clazz = Classes.forName(fqcn); + return invokeStatic(clazz, methodName, argTypes, args); } catch (Exception e) { String msg = "Unable to invoke class method " + fqcn + "#" + methodName + ". Ensure the necessary " + - "implementation is in the runtime classpath."; + "implementation is in the runtime classpath."; + throw new IllegalStateException(msg, e); + } + } + + /** + * Invokes the {@code clazz}'s matching static method (named {@code methodName} with exact argument types + * of {@code argTypes}) with the given {@code args} arguments, and returns the method return value. + * + * @param clazz the class to invoke + * @param methodName the name of the static method on {@code clazz} to invoke + * @param argTypes the types of the arguments accepted by the method + * @param args the actual runtime arguments to use when invoking the method + * @param the type of object expected to be returned from the method + * @return the result returned by the invoked method. + * @since JJWT_RELEASE_VERSION + */ + @SuppressWarnings("unchecked") + public static T invokeStatic(Class clazz, String methodName, Class[] argTypes, Object... args) { + try { + Method method = clazz.getDeclaredMethod(methodName, argTypes); + method.setAccessible(true); + return (T) method.invoke(null, args); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw ((RuntimeException) cause); //propagate + } + String msg = "Unable to invoke class method " + clazz.getName() + "#" + methodName + + ". Ensure the necessary implementation is in the runtime classpath."; throw new IllegalStateException(msg, e); } } @@ -215,8 +308,8 @@ public final class Classes { /** * @since 1.0 */ - private static interface ClassLoaderAccessor { - Class loadClass(String fqcn); + private interface ClassLoaderAccessor { + Class loadClass(String fqcn); InputStream getResourceStream(String name); } @@ -226,8 +319,8 @@ public final class Classes { */ private static abstract class ExceptionIgnoringAccessor implements ClassLoaderAccessor { - public Class loadClass(String fqcn) { - Class clazz = null; + public Class loadClass(String fqcn) { + Class clazz = null; ClassLoader cl = getClassLoader(); if (cl != null) { try { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 86cdd50d..2ca8f5e2 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -20,22 +20,155 @@ import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; +/** + * Utility methods for working with {@link Collection}s, {@link List}s, {@link Set}s, and {@link Maps}. + */ +@SuppressWarnings({"unused", "rawtypes"}) public final class Collections { - private Collections(){} //prevent instantiation + private Collections() { + } //prevent instantiation + + /** + * Returns a type-safe immutable empty {@code List}. + * + * @param list element type + * @return a type-safe immutable empty {@code List}. + */ + public static List emptyList() { + return java.util.Collections.emptyList(); + } + + /** + * Returns a type-safe immutable empty {@code Set}. + * + * @param set element type + * @return a type-safe immutable empty {@code Set}. + */ + @SuppressWarnings("unused") + public static Set emptySet() { + return java.util.Collections.emptySet(); + } + + /** + * Returns a type-safe immutable empty {@code Map}. + * + * @param map key type + * @param map value type + * @return a type-safe immutable empty {@code Map}. + */ + @SuppressWarnings("unused") + public static Map emptyMap() { + return java.util.Collections.emptyMap(); + } + + /** + * Returns a type-safe immutable {@code List} containing the specified array elements. + * + * @param elements array elements to include in the list + * @param list element type + * @return a type-safe immutable {@code List} containing the specified array elements. + */ + @SafeVarargs + public static List of(T... elements) { + if (elements == null || elements.length == 0) { + return java.util.Collections.emptyList(); + } + return java.util.Collections.unmodifiableList(Arrays.asList(elements)); + } + + /** + * Returns a type-safe immutable {@code Set} containing the specified array elements. + * + * @param elements array elements to include in the set + * @param set element type + * @return a type-safe immutable {@code Set} containing the specified array elements. + */ + @SafeVarargs + public static Set setOf(T... elements) { + if (elements == null || elements.length == 0) { + return java.util.Collections.emptySet(); + } + Set set = new LinkedHashSet<>(Arrays.asList(elements)); + return immutable(set); + } + + /** + * Shorter null-safe convenience alias for {@link java.util.Collections#unmodifiableList(List)} so both classes + * don't need to be imported. + * + * @param m map to wrap in an immutable/unmodifiable collection + * @param map key type + * @param map value type + * @return an immutable wrapper for {@code m}. + * @since JJWT_RELEASE_VERSION + */ + public static Map immutable(Map m) { + return m != null ? java.util.Collections.unmodifiableMap(m) : null; + } + + /** + * Shorter null-safe convenience alias for {@link java.util.Collections#unmodifiableSet(Set)} so both classes don't + * need to be imported. + * + * @param set set to wrap in an immutable Set + * @param set element type + * @return an immutable wrapper for {@code set} + */ + public static Set immutable(Set set) { + return set != null ? java.util.Collections.unmodifiableSet(set) : null; + } + + /** + * Shorter null-safe convenience alias for {@link java.util.Collections#unmodifiableList(List)} so both classes + * don't need to be imported. + * + * @param list list to wrap in an immutable List + * @param list element type + * @return an immutable wrapper for {@code list} + */ + public static List immutable(List list) { + return list != null ? java.util.Collections.unmodifiableList(list) : null; + } + + /** + * Null-safe factory method that returns an immutable/unmodifiable view of the specified collection instance. + * Works for {@link List}, {@link Set} and {@link Collection} arguments. + * + * @param c collection to wrap in an immutable/unmodifiable collection + * @param type of collection + * @param type of elements in the collection + * @return an immutable wrapper for {@code l}. + * @since JJWT_RELEASE_VERSION + */ + @SuppressWarnings("unchecked") + public static > C immutable(C c) { + if (c == null) { + return null; + } else if (c instanceof Set) { + return (C) java.util.Collections.unmodifiableSet((Set) c); + } else if (c instanceof List) { + return (C) java.util.Collections.unmodifiableList((List) c); + } else { + return (C) java.util.Collections.unmodifiableCollection(c); + } + } /** * Return true if the supplied Collection is null * or empty. Otherwise, return false. + * * @param collection the Collection to check * @return whether the given Collection is empty */ - public static boolean isEmpty(Collection collection) { - return (collection == null || collection.isEmpty()); + public static boolean isEmpty(Collection collection) { + return size(collection) == 0; } /** @@ -45,7 +178,7 @@ public final class Collections { * @return the collection's size or {@code 0} if the collection is {@code null}. * @since 0.9.2 */ - public static int size(Collection collection) { + public static int size(Collection collection) { return collection == null ? 0 : collection.size(); } @@ -56,18 +189,19 @@ public final class Collections { * @return the map's size or {@code 0} if the map is {@code null}. * @since 0.9.2 */ - public static int size(Map map) { + public static int size(Map map) { return map == null ? 0 : map.size(); } /** * Return true if the supplied Map is null * or empty. Otherwise, return false. + * * @param map the Map to check * @return whether the given Map is empty */ - public static boolean isEmpty(Map map) { - return (map == null || map.isEmpty()); + public static boolean isEmpty(Map map) { + return size(map) == 0; } /** @@ -75,6 +209,7 @@ public final class Collections { * converted into a List of the appropriate wrapper type. *

A null source value will be converted to an * empty List. + * * @param source the (potentially primitive) array * @return the converted List result * @see Objects#toObjectArray(Object) @@ -83,9 +218,28 @@ public final class Collections { return Arrays.asList(Objects.toObjectArray(source)); } + /** + * Concatenate the specified set with the specified array elements, resulting in a new {@link LinkedHashSet} with + * the array elements appended to the end of the existing Set. + * + * @param c the set to append to + * @param elements the array elements to append to the end of the set + * @param set element type + * @return a new {@link LinkedHashSet} with the array elements appended to the end of the original set. + */ + @SafeVarargs + public static Set concat(Set c, T... elements) { + int size = Math.max(1, Collections.size(c) + io.jsonwebtoken.lang.Arrays.length(elements)); + Set set = new LinkedHashSet<>(size); + set.addAll(c); + java.util.Collections.addAll(set, elements); + return immutable(set); + } + /** * Merge the given array into the given Collection. - * @param array the array to merge (may be null) + * + * @param array the array to merge (may be null) * @param collection the target Collection to merge the array into */ @SuppressWarnings("unchecked") @@ -94,9 +248,7 @@ public final class Collections { throw new IllegalArgumentException("Collection must not be null"); } Object[] arr = Objects.toObjectArray(array); - for (Object elem : arr) { - collection.add(elem); - } + java.util.Collections.addAll(collection, arr); } /** @@ -104,8 +256,9 @@ public final class Collections { * copying all properties (key-value pairs) over. *

Uses Properties.propertyNames() to even catch * default properties linked into the original Properties instance. + * * @param props the Properties instance to merge (may be null) - * @param map the target Map to merge the properties into + * @param map the target Map to merge the properties into */ @SuppressWarnings("unchecked") public static void mergePropertiesIntoMap(Properties props, Map map) { @@ -113,7 +266,7 @@ public final class Collections { throw new IllegalArgumentException("Map must not be null"); } if (props != null) { - for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements(); ) { String key = (String) en.nextElement(); Object value = props.getProperty(key); if (value == null) { @@ -128,8 +281,9 @@ public final class Collections { /** * Check whether the given Iterator contains the given element. + * * @param iterator the Iterator to check - * @param element the element to look for + * @param element the element to look for * @return true if found, false else */ public static boolean contains(Iterator iterator, Object element) { @@ -146,8 +300,9 @@ public final class Collections { /** * Check whether the given Enumeration contains the given element. + * * @param enumeration the Enumeration to check - * @param element the element to look for + * @param element the element to look for * @return true if found, false else */ public static boolean contains(Enumeration enumeration, Object element) { @@ -166,8 +321,9 @@ public final class Collections { * Check whether the given Collection contains the given element instance. *

Enforces the given instance to be present, rather than returning * true for an equal element as well. + * * @param collection the Collection to check - * @param element the element to look for + * @param element the element to look for * @return true if found, false else */ public static boolean containsInstance(Collection collection, Object element) { @@ -184,7 +340,8 @@ public final class Collections { /** * Return true if any element in 'candidates' is * contained in 'source'; otherwise returns false. - * @param source the source Collection + * + * @param source the source Collection * @param candidates the candidates to search for * @return whether any of the candidates has been found */ @@ -205,7 +362,8 @@ public final class Collections { * 'source'. If no element in 'candidates' is present in * 'source' returns null. Iteration order is * {@link Collection} implementation specific. - * @param source the source Collection + * + * @param source the source Collection * @param candidates the candidates to search for * @return the first present object, or null if not found */ @@ -223,9 +381,10 @@ public final class Collections { /** * Find a single value of the given type in the given Collection. + * * @param collection the Collection to search - * @param type the type to look for - * @param the generic type parameter for {@code type} + * @param type the type to look for + * @param the generic type parameter for {@code type} * @return a value of the given type found if there is a clear match, * or null if none or more than one such value found */ @@ -251,8 +410,9 @@ public final class Collections { * Find a single value of one of the given types in the given Collection: * searching the Collection for a value of the first type, then * searching for a value of the second type, etc. + * * @param collection the collection to search - * @param types the types to look for, in prioritized order + * @param types the types to look for, in prioritized order * @return a value of one of the given types found if there is a clear match, * or null if none or more than one such value found */ @@ -271,6 +431,7 @@ public final class Collections { /** * Determine whether the given Collection only contains a single unique object. + * * @param collection the Collection to check * @return true if the collection contains a single reference or * multiple references to the same instance, false else @@ -285,8 +446,7 @@ public final class Collections { if (!hasCandidate) { hasCandidate = true; candidate = elem; - } - else if (candidate != elem) { + } else if (candidate != elem) { return false; } } @@ -295,6 +455,7 @@ public final class Collections { /** * Find the common element type of the given Collection, if any. + * * @param collection the Collection to check * @return the common element type, or null if no clear * common type has been found (or the collection was empty) @@ -308,8 +469,7 @@ public final class Collections { if (val != null) { if (candidate == null) { candidate = val.getClass(); - } - else if (candidate != val.getClass()) { + } else if (candidate != val.getClass()) { return null; } } @@ -321,14 +481,15 @@ public final class Collections { * Marshal the elements from the given enumeration into an array of the given type. * Enumeration elements must be assignable to the type of the given array. The array * returned will be a different instance than the array given. + * * @param enumeration the collection to convert to an array - * @param array an array instance that matches the type of array to return - * @param the element type of the array that will be created - * @param the element type contained within the enumeration. + * @param array an array instance that matches the type of array to return + * @param the element type of the array that will be created + * @param the element type contained within the enumeration. * @return a new array of type {@code A} that contains the elements in the specified {@code enumeration}. */ - public static A[] toArray(Enumeration enumeration, A[] array) { - ArrayList elements = new ArrayList(); + public static A[] toArray(Enumeration enumeration, A[] array) { + ArrayList elements = new ArrayList<>(); while (enumeration.hasMoreElements()) { elements.add(enumeration.nextElement()); } @@ -337,12 +498,13 @@ public final class Collections { /** * Adapt an enumeration to an iterator. + * * @param enumeration the enumeration - * @param the type of elements in the enumeration + * @param the type of elements in the enumeration * @return the iterator */ public static Iterator toIterator(Enumeration enumeration) { - return new EnumerationIterator(enumeration); + return new EnumerationIterator<>(enumeration); } /** @@ -350,7 +512,7 @@ public final class Collections { */ private static class EnumerationIterator implements Iterator { - private Enumeration enumeration; + private final Enumeration enumeration; public EnumerationIterator(Enumeration enumeration) { this.enumeration = enumeration; diff --git a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java index 250d9892..6a3b501b 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java +++ b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java @@ -22,9 +22,14 @@ import java.util.Date; import java.util.TimeZone; /** + * Utility methods to format and parse date strings. + * * @since 0.10.0 */ -public class DateFormats { +public final class DateFormats { + + private DateFormats() { + } // prevent instantiation private static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; @@ -48,10 +53,25 @@ public class DateFormats { } }; + /** + * Return an ISO-8601-formatted string with millisecond precision representing the + * specified {@code date}. + * + * @param date the date for which to create an ISO-8601-formatted string + * @return the date represented as an ISO-8601-formatted string with millisecond precision. + */ public static String formatIso8601(Date date) { return formatIso8601(date, true); } + /** + * Returns an ISO-8601-formatted string with optional millisecond precision for the specified + * {@code date}. + * + * @param date the date for which to create an ISO-8601-formatted string + * @param includeMillis whether to include millisecond notation within the string. + * @return the date represented as an ISO-8601-formatted string with optional millisecond precision. + */ public static String formatIso8601(Date date, boolean includeMillis) { if (includeMillis) { return ISO_8601_MILLIS.get().format(date); @@ -59,6 +79,14 @@ public class DateFormats { return ISO_8601.get().format(date); } + /** + * Parse the specified ISO-8601-formatted date string and return the corresponding {@link Date} instance. The + * date string may optionally contain millisecond notation, and those milliseconds will be represented accordingly. + * + * @param s the ISO-8601-formatted string to parse + * @return the string's corresponding {@link Date} instance. + * @throws ParseException if the specified date string is not a validly-formatted ISO-8601 string. + */ public static Date parseIso8601Date(String s) throws ParseException { Assert.notNull(s, "String argument cannot be null."); if (s.lastIndexOf('.') > -1) { //assume ISO-8601 with milliseconds diff --git a/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java b/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java index 0ee8418f..d6b414d7 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java +++ b/api/src/main/java/io/jsonwebtoken/lang/InstantiationException.java @@ -16,11 +16,19 @@ package io.jsonwebtoken.lang; /** + * {@link RuntimeException} equivalent of {@link java.lang.InstantiationException}. + * * @since 0.1 */ public class InstantiationException extends RuntimeException { - public InstantiationException(String s, Throwable t) { - super(s, t); + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public InstantiationException(String message, Throwable cause) { + super(message, cause); } } diff --git a/api/src/main/java/io/jsonwebtoken/lang/MapMutator.java b/api/src/main/java/io/jsonwebtoken/lang/MapMutator.java new file mode 100644 index 00000000..b16b95fa --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/MapMutator.java @@ -0,0 +1,65 @@ +/* + * 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.lang; + +import java.util.Map; + +/** + * Mutation (modifications) to a {@link Map} instance while also supporting method chaining. The Map interface's + * {@link Map#put(Object, Object)}, {@link Map#remove(Object)}, {@link Map#putAll(Map)}, and {@link Map#clear()} + * mutation methods do not support method chaining, so this interface enables that behavior. + * + * @param map key type + * @param map value type + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface MapMutator> { + + /** + * Sets the specified name/value pair in the map. A {@code null} or empty value will remove the property + * from the map entirely. + * + * @param key the map key + * @param value the value to set for the specified header parameter name + * @return the mutator/builder for method chaining. + */ + T put(K key, V value); + + /** + * Removes the map entry with the specified key + * + * @param key the key for the map entry to remove. + * @return the mutator/builder for method chaining. + */ + T remove(K key); + + /** + * Sets the specified name/value pairs in the map. If any name has a {@code null} or empty value, that + * map entry will be removed from the map entirely. + * + * @param m the map to add + * @return the mutator/builder for method chaining. + */ + T putAll(Map m); + + /** + * Removes all entries from the map. The map will be empty after this call returns. + * + * @return the mutator/builder for method chaining. + */ + T clear(); +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/Maps.java b/api/src/main/java/io/jsonwebtoken/lang/Maps.java index 6f7d8fa4..aef613af 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Maps.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Maps.java @@ -21,11 +21,13 @@ import java.util.Map; /** * Utility class to help with the manipulation of working with Maps. + * * @since 0.11.0 */ public final class Maps { - private Maps() {} //prevent instantiation + private Maps() { + } //prevent instantiation /** * Creates a new map builder with a single entry. @@ -35,7 +37,8 @@ public final class Maps { * // ... * .build(); * } - * @param key the key of an map entry to be added + * + * @param key the key of an map entry to be added * @param value the value of map entry to be added * @param the maps key type * @param the maps value type @@ -53,21 +56,24 @@ public final class Maps { * // ... * .build(); * } + * * @param the maps key type * @param the maps value type */ - public interface MapBuilder { + public interface MapBuilder extends Builder> { /** * Add a new entry to this map builder - * @param key the key of an map entry to be added + * + * @param key the key of an map entry to be added * @param value the value of map entry to be added * @return the current MapBuilder to allow for method chaining. */ MapBuilder and(K key, V value); /** - * Returns a the resulting Map object from this MapBuilder. - * @return Returns a the resulting Map object from this MapBuilder. + * Returns the resulting Map object from this MapBuilder. + * + * @return the resulting Map object from this MapBuilder. */ Map build(); } @@ -80,6 +86,7 @@ public final class Maps { data.put(key, value); return this; } + public Map build() { return Collections.unmodifiableMap(data); } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Objects.java b/api/src/main/java/io/jsonwebtoken/lang/Objects.java index 4dbc80f9..3787c218 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -20,18 +20,23 @@ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; +/** + * Utility methods for working with object instances to reduce pattern repetition and otherwise + * increased cyclomatic complexity. + */ public final class Objects { - private Objects(){} //prevent instantiation + private Objects() { + } //prevent instantiation private static final int INITIAL_HASH = 7; - private static final int MULTIPLIER = 31; + private static final int MULTIPLIER = 31; - private static final String EMPTY_STRING = ""; - private static final String NULL_STRING = "null"; - private static final String ARRAY_START = "{"; - private static final String ARRAY_END = "}"; - private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; private static final String ARRAY_ELEMENT_SEPARATOR = ", "; /** @@ -102,6 +107,16 @@ public final class Objects { return array == null || array.length == 0; } + /** + * Returns {@code true} if the specified character array is null or of zero length, {@code false} otherwise. + * + * @param chars the character array to check + * @return {@code true} if the specified character array is null or of zero length, {@code false} otherwise. + */ + public static boolean isEmpty(char[] chars) { + return chars == null || chars.length == 0; + } + /** * Check whether the given array contains the given element. * @@ -145,8 +160,8 @@ public final class Objects { public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { for (Enum candidate : enumValues) { if (caseSensitive ? - candidate.toString().equals(constant) : - candidate.toString().equalsIgnoreCase(constant)) { + candidate.toString().equals(constant) : + candidate.toString().equalsIgnoreCase(constant)) { return true; } } @@ -171,8 +186,8 @@ public final class Objects { } } throw new IllegalArgumentException( - String.format("constant [%s] does not exist in enum type %s", - constant, enumValues.getClass().getComponentType().getName())); + String.format("constant [%s] does not exist in enum type %s", + constant, enumValues.getClass().getComponentType().getName())); } /** @@ -180,9 +195,9 @@ public final class Objects { * consisting of the input array contents plus the given object. * * @param array the array to append to (can be null) - * @param the type of each element in the specified {@code array} + * @param the type of each element in the specified {@code array} * @param obj the object to append - * @param the type of the specified object, which must equal to or extend the {@code <A>} type. + * @param the type of the specified object, which must be equal to or extend the <A> type. * @return the new array (of the same component type; never null) */ public static A[] addObjectToArray(A[] array, O obj) { @@ -300,6 +315,8 @@ public final class Objects { * methods for arrays in this class. If the object is null, * this method returns 0. * + * @param obj the object to use for obtaining a hashcode + * @return the object's hashcode, which could be 0 if the object is null. * @see #nullSafeHashCode(Object[]) * @see #nullSafeHashCode(boolean[]) * @see #nullSafeHashCode(byte[]) @@ -309,8 +326,6 @@ public final class Objects { * @see #nullSafeHashCode(int[]) * @see #nullSafeHashCode(long[]) * @see #nullSafeHashCode(short[]) - * @param obj the object to use for obtaining a hashcode - * @return the object's hashcode, which could be 0 if the object is null. */ public static int nullSafeHashCode(Object obj) { if (obj == null) { @@ -351,10 +366,11 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the array to obtain a hashcode * @return the array's hashcode, which could be 0 if the array is null. */ - public static int nullSafeHashCode(Object[] array) { + public static int nullSafeHashCode(Object... array) { if (array == null) { return 0; } @@ -369,6 +385,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the boolean array to obtain a hashcode * @return the boolean array's hashcode, which could be 0 if the array is null. */ @@ -387,6 +404,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the byte array to obtain a hashcode * @return the byte array's hashcode, which could be 0 if the array is null. */ @@ -405,6 +423,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the char array to obtain a hashcode * @return the char array's hashcode, which could be 0 if the array is null. */ @@ -423,6 +442,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the double array to obtain a hashcode * @return the double array's hashcode, which could be 0 if the array is null. */ @@ -441,6 +461,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the float array to obtain a hashcode * @return the float array's hashcode, which could be 0 if the array is null. */ @@ -459,6 +480,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the int array to obtain a hashcode * @return the int array's hashcode, which could be 0 if the array is null. */ @@ -477,6 +499,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the long array to obtain a hashcode * @return the long array's hashcode, which could be 0 if the array is null. */ @@ -495,6 +518,7 @@ public final class Objects { /** * Return a hash code based on the contents of the specified array. * If array is null, this method returns 0. + * * @param array the short array to obtain a hashcode * @return the short array's hashcode, which could be 0 if the array is null. */ @@ -939,6 +963,12 @@ public final class Objects { return sb.toString(); } + /** + * Iterate over the specified {@link Closeable} instances, invoking + * {@link Closeable#close()} on each one, ignoring any potential {@link IOException}s. + * + * @param closeables the closeables to close. + */ public static void nullSafeClose(Closeable... closeables) { if (closeables == null) { return; diff --git a/api/src/main/java/io/jsonwebtoken/lang/Registry.java b/api/src/main/java/io/jsonwebtoken/lang/Registry.java new file mode 100644 index 00000000..e157fba5 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Registry.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2020 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.lang; + +import java.util.Collection; + +/** + * An immutable read-only repository of key-value pairs. + * + * @param key type + * @param value type + * @since JJWT_RELEASE_VERSION + */ +public interface Registry { + + /** + * Returns all registry values as a read-only collection. + * + * @return all registry values as a read-only collection. + */ + Collection values(); + + /** + * Returns the value assigned the specified key or throws an {@code IllegalArgumentException} if there is no + * associated value. If a value is not required, consider using the {@link #find(Object)} method instead. + * + * @param key the registry key assigned to the required value + * @return the value assigned the specified key + * @throws IllegalArgumentException if there is no value assigned the specified key + * @see #find(Object) + */ + V get(K key) throws IllegalArgumentException; + + /** + * Returns the value assigned the specified key or {@code null} if there is no associated value. + * + * @param key the registry key assigned to the required value + * @return the value assigned the specified key or {@code null} if there is no associated value. + * @see #get(Object) + */ + V find(K key); +} diff --git a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java index df5b8c7c..1bb30162 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java +++ b/api/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java @@ -19,16 +19,36 @@ import java.security.Provider; import java.security.Security; import java.util.concurrent.atomic.AtomicBoolean; +/** + * No longer used by JJWT. Will be removed before the 1.0 final release. + * + * @deprecated since JJWT_RELEASE_VERSION. will be removed before the 1.0 final release. + */ +@Deprecated public final class RuntimeEnvironment { - private RuntimeEnvironment(){} //prevent instantiation + private RuntimeEnvironment() { + } //prevent instantiation private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider"; private static final AtomicBoolean bcLoaded = new AtomicBoolean(false); + /** + * {@code true} if BouncyCastle is in the runtime classpath, {@code false} otherwise. + * + * @deprecated since JJWT_RELEASE_VERSION. will be removed before the 1.0 final release. + */ + @Deprecated public static final boolean BOUNCY_CASTLE_AVAILABLE = Classes.isAvailable(BC_PROVIDER_CLASS_NAME); + /** + * Register BouncyCastle as a JCA provider in the system's {@link Security#getProviders() Security Providers} list + * if BouncyCastle is in the runtime classpath. + * + * @deprecated since JJWT_RELEASE_VERSION. will be removed before the 1.0 final release. + */ + @Deprecated public static void enableBouncyCastleIfPossible() { if (!BOUNCY_CASTLE_AVAILABLE || bcLoaded.get()) { @@ -36,13 +56,13 @@ public final class RuntimeEnvironment { } try { - Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); + Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); //check to see if the user has already registered the BC provider: Provider[] providers = Security.getProviders(); - for(Provider provider : providers) { + for (Provider provider : providers) { if (clazz.isInstance(provider)) { bcLoaded.set(true); return; @@ -50,7 +70,8 @@ public final class RuntimeEnvironment { } //bc provider not enabled - add it: - Security.addProvider((Provider)Classes.newInstance(clazz)); + Provider provider = Classes.newInstance(clazz); + Security.addProvider(provider); bcLoaded.set(true); } catch (UnknownClassException e) { diff --git a/api/src/main/java/io/jsonwebtoken/lang/Strings.java b/api/src/main/java/io/jsonwebtoken/lang/Strings.java index da2d1d22..35b22595 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.lang; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -29,8 +30,17 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; +/** + * Utility methods for working with Strings to reduce pattern repetition and otherwise + * increased cyclomatic complexity. + */ public final class Strings { + /** + * Empty String, equal to "". + */ + public static final String EMPTY = ""; + private static final String FOLDER_SEPARATOR = "/"; private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; @@ -41,9 +51,13 @@ public final class Strings { private static final char EXTENSION_SEPARATOR = '.'; - public static final Charset UTF_8 = Charset.forName("UTF-8"); + /** + * Convenience alias for {@link StandardCharsets#UTF_8}. + */ + public static final Charset UTF_8 = StandardCharsets.UTF_8; - private Strings(){} //prevent instantiation + private Strings() { + } //prevent instantiation //--------------------------------------------------------------------- // General convenience methods for working with Strings @@ -58,6 +72,7 @@ public final class Strings { * Strings.hasLength(" ") = true * Strings.hasLength("Hello") = true * + * * @param str the CharSequence to check (may be null) * @return true if the CharSequence is not null and has length * @see #hasText(String) @@ -69,6 +84,7 @@ public final class Strings { /** * Check that the given String is neither null nor of length 0. * Note: Will return true for a String that purely consists of whitespace. + * * @param str the String to check (may be null) * @return true if the String is not null and has length * @see #hasLength(CharSequence) @@ -88,6 +104,7 @@ public final class Strings { * Strings.hasText("12345") = true * Strings.hasText(" 12345 ") = true * + * * @param str the CharSequence to check (may be null) * @return true if the CharSequence is not null, * its length is greater than 0, and it does not contain whitespace only @@ -110,6 +127,7 @@ public final class Strings { * Check whether the given String has actual text. * More specifically, returns true if the string not null, * its length is greater than 0, and it contains at least one non-whitespace character. + * * @param str the String to check (may be null) * @return true if the String is not null, its length is * greater than 0, and it does not contain whitespace only @@ -121,6 +139,7 @@ public final class Strings { /** * Check whether the given CharSequence contains any whitespace characters. + * * @param str the CharSequence to check (may be null) * @return true if the CharSequence is not empty and * contains at least 1 whitespace character @@ -141,6 +160,7 @@ public final class Strings { /** * Check whether the given String contains any whitespace characters. + * * @param str the String to check (may be null) * @return true if the String is not empty and * contains at least 1 whitespace character @@ -152,15 +172,16 @@ public final class Strings { /** * Trim leading and trailing whitespace from the given String. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace */ public static String trimWhitespace(String str) { - return (String) trimWhitespace((CharSequence)str); + return (String) trimWhitespace((CharSequence) str); } - - + + private static CharSequence trimWhitespace(CharSequence str) { if (!hasLength(str)) { return str; @@ -168,24 +189,40 @@ public final class Strings { final int length = str.length(); int start = 0; - while (start < length && Character.isWhitespace(str.charAt(start))) { + while (start < length && Character.isWhitespace(str.charAt(start))) { start++; } - - int end = length; + + int end = length; while (start < length && Character.isWhitespace(str.charAt(end - 1))) { end--; } - + return ((start > 0) || (end < length)) ? str.subSequence(start, end) : str; } + /** + * Returns the specified string without leading or trailing whitespace, or {@code null} if there are no remaining + * characters. + * + * @param str the string to clean + * @return the specified string without leading or trailing whitespace, or {@code null} if there are no remaining + * characters. + */ public static String clean(String str) { - CharSequence result = clean((CharSequence) str); - - return result!=null?result.toString():null; + CharSequence result = clean((CharSequence) str); + + return result != null ? result.toString() : null; } - + + /** + * Returns the specified {@code CharSequence} without leading or trailing whitespace, or {@code null} if there are + * no remaining characters. + * + * @param str the {@code CharSequence} to clean + * @return the specified string without leading or trailing whitespace, or {@code null} if there are no remaining + * characters. + */ public static CharSequence clean(CharSequence str) { str = trimWhitespace(str); if (!hasLength(str)) { @@ -194,9 +231,56 @@ public final class Strings { return str; } + /** + * Returns a String representation (1s and 0s) of the specified byte. + * + * @param b the byte to represent as 1s and 0s. + * @return a String representation (1s and 0s) of the specified byte. + */ + public static String toBinary(byte b) { + String bString = Integer.toBinaryString(b & 0xFF); + return String.format("%8s", bString).replace((char) Character.SPACE_SEPARATOR, '0'); + } + + /** + * Returns a String representation (1s and 0s) of the specified byte array. + * + * @param bytes the bytes to represent as 1s and 0s. + * @return a String representation (1s and 0s) of the specified byte array. + */ + public static String toBinary(byte[] bytes) { + StringBuilder sb = new StringBuilder(19); //16 characters + 3 space characters + for (byte b : bytes) { + if (sb.length() > 0) { + sb.append((char) Character.SPACE_SEPARATOR); + } + String val = toBinary(b); + sb.append(val); + } + return sb.toString(); + } + + /** + * Returns a hexadecimal String representation of the specified byte array. + * + * @param bytes the bytes to represent as a hexidecimal string. + * @return a hexadecimal String representation of the specified byte array. + */ + public static String toHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte temp : bytes) { + if (result.length() > 0) { + result.append((char) Character.SPACE_SEPARATOR); + } + result.append(String.format("%02x", temp)); + } + return result.toString(); + } + /** * Trim all whitespace from the given String: * leading, trailing, and intermediate characters. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace @@ -210,8 +294,7 @@ public final class Strings { while (sb.length() > index) { if (Character.isWhitespace(sb.charAt(index))) { sb.deleteCharAt(index); - } - else { + } else { index++; } } @@ -220,6 +303,7 @@ public final class Strings { /** * Trim leading whitespace from the given String. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace @@ -237,6 +321,7 @@ public final class Strings { /** * Trim trailing whitespace from the given String. + * * @param str the String to check * @return the trimmed String * @see java.lang.Character#isWhitespace @@ -254,7 +339,8 @@ public final class Strings { /** * Trim all occurrences of the supplied leading character from the given String. - * @param str the String to check + * + * @param str the String to check * @param leadingCharacter the leading character to be trimmed * @return the trimmed String */ @@ -271,7 +357,8 @@ public final class Strings { /** * Trim all occurrences of the supplied trailing character from the given String. - * @param str the String to check + * + * @param str the String to check * @param trailingCharacter the trailing character to be trimmed * @return the trimmed String */ @@ -289,7 +376,8 @@ public final class Strings { /** * Returns {@code true} if the given string starts with the specified case-insensitive prefix, {@code false} otherwise. - * @param str the String to check + * + * @param str the String to check * @param prefix the prefix to look for * @return {@code true} if the given string starts with the specified case-insensitive prefix, {@code false} otherwise. * @see java.lang.String#startsWith @@ -298,12 +386,12 @@ public final class Strings { if (str == null || prefix == null) { return false; } - if (str.startsWith(prefix)) { - return true; - } if (str.length() < prefix.length()) { return false; } + if (str.startsWith(prefix)) { + return true; + } String lcStr = str.substring(0, prefix.length()).toLowerCase(); String lcPrefix = prefix.toLowerCase(); return lcStr.equals(lcPrefix); @@ -311,7 +399,8 @@ public final class Strings { /** * Returns {@code true} if the given string ends with the specified case-insensitive suffix, {@code false} otherwise. - * @param str the String to check + * + * @param str the String to check * @param suffix the suffix to look for * @return {@code true} if the given string ends with the specified case-insensitive suffix, {@code false} otherwise. * @see java.lang.String#endsWith @@ -334,8 +423,9 @@ public final class Strings { /** * Returns {@code true} if the given string matches the given substring at the given index, {@code false} otherwise. - * @param str the original string (or StringBuilder) - * @param index the index in the original string to start matching against + * + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against * @param substring the substring to match at the given index * @return {@code true} if the given string matches the given substring at the given index, {@code false} otherwise. */ @@ -351,6 +441,7 @@ public final class Strings { /** * Returns the number of occurrences the substring {@code sub} appears in string {@code str}. + * * @param str string to search in. Return 0 if this is null. * @param sub string to search for. Return 0 if this is null. * @return the number of occurrences the substring {@code sub} appears in string {@code str}. @@ -372,7 +463,8 @@ public final class Strings { /** * Replace all occurrences of a substring within a string with * another string. - * @param inString String to examine + * + * @param inString String to examine * @param oldPattern String to replace * @param newPattern String to insert * @return a String with the replacements @@ -399,8 +491,9 @@ public final class Strings { /** * Delete all occurrences of the given substring. + * * @param inString the original String - * @param pattern the pattern to delete all occurrences of + * @param pattern the pattern to delete all occurrences of * @return the resulting String */ public static String delete(String inString, String pattern) { @@ -409,9 +502,10 @@ public final class Strings { /** * Delete any character in a given String. - * @param inString the original String + * + * @param inString the original String * @param charsToDelete a set of characters to delete. - * E.g. "az\n" will delete 'a's, 'z's and new lines. + * E.g. "az\n" will delete 'a's, 'z's and new lines. * @return the resulting String */ public static String deleteAny(String inString, String charsToDelete) { @@ -435,6 +529,7 @@ public final class Strings { /** * Quote the given String with single quotes. + * * @param str the input String (e.g. "myString") * @return the quoted String (e.g. "'myString'"), * or null if the input was null @@ -446,6 +541,7 @@ public final class Strings { /** * Turn the given Object into a String with single quotes * if it is a String; keeping the Object as-is else. + * * @param obj the input Object (e.g. "myString") * @return the quoted String (e.g. "'myString'"), * or the input object as-is if not a String @@ -457,6 +553,7 @@ public final class Strings { /** * Unqualify a string qualified by a '.' dot character. For example, * "this.name.is.qualified", returns "qualified". + * * @param qualifiedName the qualified name * @return an unqualified string by stripping all previous text before (and including) the last period character. */ @@ -467,8 +564,9 @@ public final class Strings { /** * Unqualify a string qualified by a separator character. For example, * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * * @param qualifiedName the qualified name - * @param separator the separator + * @param separator the separator * @return an unqualified string by stripping all previous text before and including the last {@code separator} character. */ public static String unqualify(String qualifiedName, char separator) { @@ -479,6 +577,7 @@ public final class Strings { * Capitalize a String, changing the first letter to * upper case as per {@link Character#toUpperCase(char)}. * No other letters are changed. + * * @param str the String to capitalize, may be null * @return the capitalized String, null if null */ @@ -490,6 +589,7 @@ public final class Strings { * Uncapitalize a String, changing the first letter to * lower case as per {@link Character#toLowerCase(char)}. * No other letters are changed. + * * @param str the String to uncapitalize, may be null * @return the uncapitalized String, null if null */ @@ -504,8 +604,7 @@ public final class Strings { StringBuilder sb = new StringBuilder(str.length()); if (capitalize) { sb.append(Character.toUpperCase(str.charAt(0))); - } - else { + } else { sb.append(Character.toLowerCase(str.charAt(0))); } sb.append(str.substring(1)); @@ -515,6 +614,7 @@ public final class Strings { /** * Extract the filename from the given path, * e.g. "mypath/myfile.txt" -> "myfile.txt". + * * @param path the file path (may be null) * @return the extracted filename, or null if none */ @@ -529,6 +629,7 @@ public final class Strings { /** * Extract the filename extension from the given path, * e.g. "mypath/myfile.txt" -> "txt". + * * @param path the file path (may be null) * @return the extracted filename extension, or null if none */ @@ -550,6 +651,7 @@ public final class Strings { /** * Strip the filename extension from the given path, * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * * @param path the file path (may be null) * @return the path with stripped filename extension, * or null if none @@ -572,9 +674,10 @@ public final class Strings { /** * Apply the given relative path to the given path, * assuming standard Java folder separation (i.e. "/" separators). - * @param path the path to start from (usually a full file path) + * + * @param path the path to start from (usually a full file path) * @param relativePath the relative path to apply - * (relative to the full file path above) + * (relative to the full file path above) * @return the full file path that results from applying the relative path */ public static String applyRelativePath(String path, String relativePath) { @@ -585,8 +688,7 @@ public final class Strings { newPath += FOLDER_SEPARATOR; } return newPath + relativePath; - } - else { + } else { return relativePath; } } @@ -596,6 +698,7 @@ public final class Strings { * inner simple dots. *

The result is convenient for path comparison. For other uses, * notice that Windows separators ("\") are replaced by simple slashes. + * * @param path the original path * @return the normalized path */ @@ -628,17 +731,14 @@ public final class Strings { String element = pathArray[i]; if (CURRENT_PATH.equals(element)) { // Points to current directory - drop it. - } - else if (TOP_PATH.equals(element)) { + } else if (TOP_PATH.equals(element)) { // Registering top path found. tops++; - } - else { + } else { if (tops > 0) { // Merging path element with element corresponding to top path. tops--; - } - else { + } else { // Normal path element found. pathElements.add(0, element); } @@ -655,6 +755,7 @@ public final class Strings { /** * Compare two paths after normalization of them. + * * @param path1 first path for comparison * @param path2 second path for comparison * @return whether the two paths are equivalent after normalization @@ -666,9 +767,10 @@ public final class Strings { /** * Parse the given localeString value into a {@link java.util.Locale}. *

This is the inverse operation of {@link java.util.Locale#toString Locale's toString}. + * * @param localeString the locale string, following Locale's - * toString() format ("en", "en_UK", etc); - * also accepts spaces as separators, as an alternative to underscores + * toString() format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores * @return a corresponding Locale instance */ public static Locale parseLocaleString(String localeString) { @@ -696,7 +798,7 @@ public final class Strings { char ch = localePart.charAt(i); if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) { throw new IllegalArgumentException( - "Locale part \"" + localePart + "\" contains invalid characters"); + "Locale part \"" + localePart + "\" contains invalid characters"); } } } @@ -704,6 +806,7 @@ public final class Strings { /** * Determine the RFC 3066 compliant language tag, * as used for the HTTP "Accept-Language" header. + * * @param locale the Locale to transform to a language tag * @return the RFC 3066 compliant language tag as String */ @@ -719,13 +822,14 @@ public final class Strings { /** * Append the given String to the given String array, returning a new array * consisting of the input array contents plus the given String. + * * @param array the array to append to (can be null) - * @param str the String to append + * @param str the String to append * @return the new array (never null) */ public static String[] addStringToArray(String[] array, String str) { if (Objects.isEmpty(array)) { - return new String[] {str}; + return new String[]{str}; } String[] newArr = new String[array.length + 1]; System.arraycopy(array, 0, newArr, 0, array.length); @@ -737,6 +841,7 @@ public final class Strings { * Concatenate the given String arrays into one, * with overlapping array elements included twice. *

The order of elements in the original arrays is preserved. + * * @param array1 the first array (can be null) * @param array2 the second array (can be null) * @return the new array (null if both given arrays were null) @@ -760,6 +865,7 @@ public final class Strings { *

The order of elements in the original arrays is preserved * (with the exception of overlapping elements, which are only * included on their first occurrence). + * * @param array1 the first array (can be null) * @param array2 the second array (can be null) * @return the new array (null if both given arrays were null) @@ -783,6 +889,7 @@ public final class Strings { /** * Turn given source String array into sorted array. + * * @param array the source array * @return the sorted array (never null) */ @@ -797,6 +904,7 @@ public final class Strings { /** * Copy the given Collection into a String array. * The Collection must contain String elements only. + * * @param collection the Collection to copy * @return the String array (null if the passed-in * Collection was null) @@ -811,6 +919,7 @@ public final class Strings { /** * Copy the given Enumeration into a String array. * The Enumeration must contain String elements only. + * * @param enumeration the Enumeration to copy * @return the String array (null if the passed-in * Enumeration was null) @@ -826,6 +935,7 @@ public final class Strings { /** * Trim the elements of the given String array, * calling String.trim() on each of them. + * * @param array the original String array * @return the resulting array (of the same size) with trimmed elements */ @@ -844,6 +954,7 @@ public final class Strings { /** * Remove duplicate Strings from the given array. * Also sorts the array, as it uses a TreeSet. + * * @param array the String array * @return an array without duplicates, in natural sort order */ @@ -861,7 +972,8 @@ public final class Strings { /** * Split a String at the first occurrence of the delimiter. * Does not include the delimiter in the result. - * @param toSplit the string to split + * + * @param toSplit the string to split * @param delimiter to split the string up with * @return a two element array with index 0 being before the delimiter, and * index 1 being after the delimiter (neither element includes the delimiter); @@ -877,7 +989,7 @@ public final class Strings { } String beforeDelimiter = toSplit.substring(0, offset); String afterDelimiter = toSplit.substring(offset + delimiter.length()); - return new String[] {beforeDelimiter, afterDelimiter}; + return new String[]{beforeDelimiter, afterDelimiter}; } /** @@ -886,7 +998,8 @@ public final class Strings { * delimiter providing the key, and the right of the delimiter providing the value. *

Will trim both the key and value before adding them to the * Properties instance. - * @param array the array to process + * + * @param array the array to process * @param delimiter to split each element using (typically the equals symbol) * @return a Properties instance representing the array contents, * or null if the array to process was null or empty @@ -901,16 +1014,17 @@ public final class Strings { * delimiter providing the key, and the right of the delimiter providing the value. *

Will trim both the key and value before adding them to the * Properties instance. - * @param array the array to process - * @param delimiter to split each element using (typically the equals symbol) + * + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) * @param charsToDelete one or more characters to remove from each element - * prior to attempting the split operation (typically the quotation mark - * symbol), or null if no removal should occur + * prior to attempting the split operation (typically the quotation mark + * symbol), or null if no removal should occur * @return a Properties instance representing the array contents, * or null if the array to process was null or empty */ public static Properties splitArrayElementsIntoProperties( - String[] array, String delimiter, String charsToDelete) { + String[] array, String delimiter, String charsToDelete) { if (Objects.isEmpty(array)) { return null; @@ -936,9 +1050,10 @@ public final class Strings { * delimiter characters. Each of those characters can be used to separate * tokens. A delimiter is always a single character; for multi-character * delimiters, consider using delimitedListToStringArray - * @param str the String to tokenize + * + * @param str the String to tokenize * @param delimiters the delimiter characters, assembled as String - * (each of those characters is individually considered as delimiter). + * (each of those characters is individually considered as delimiter). * @return an array of the tokens * @see java.util.StringTokenizer * @see java.lang.String#trim() @@ -954,13 +1069,14 @@ public final class Strings { * delimiter characters. Each of those characters can be used to separate * tokens. A delimiter is always a single character; for multi-character * delimiters, consider using delimitedListToStringArray - * @param str the String to tokenize - * @param delimiters the delimiter characters, assembled as String - * (each of those characters is individually considered as delimiter) - * @param trimTokens trim the tokens via String's trim + * + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's trim * @param ignoreEmptyTokens omit empty tokens from the result array - * (only applies to tokens that are empty after trimming; StringTokenizer - * will not consider subsequent delimiters as token in the first place). + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). * @return an array of the tokens (null if the input String * was null) * @see java.util.StringTokenizer @@ -968,7 +1084,7 @@ public final class Strings { * @see #delimitedListToStringArray */ public static String[] tokenizeToStringArray( - String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { if (str == null) { return null; @@ -992,9 +1108,10 @@ public final class Strings { *

A single delimiter can consists of more than one character: It will still * be considered as single delimiter string, rather than as bunch of potential * delimiter characters - in contrast to tokenizeToStringArray. - * @param str the input String + * + * @param str the input String * @param delimiter the delimiter between elements (this is a single delimiter, - * rather than a bunch individual delimiter characters) + * rather than a bunch individual delimiter characters) * @return an array of the tokens in the list * @see #tokenizeToStringArray */ @@ -1007,11 +1124,12 @@ public final class Strings { *

A single delimiter can consists of more than one character: It will still * be considered as single delimiter string, rather than as bunch of potential * delimiter characters - in contrast to tokenizeToStringArray. - * @param str the input String - * @param delimiter the delimiter between elements (this is a single delimiter, - * rather than a bunch individual delimiter characters) + * + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) * @param charsToDelete a set of characters to delete. Useful for deleting unwanted - * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. * @return an array of the tokens in the list * @see #tokenizeToStringArray */ @@ -1020,15 +1138,14 @@ public final class Strings { return new String[0]; } if (delimiter == null) { - return new String[] {str}; + return new String[]{str}; } List result = new ArrayList(); if ("".equals(delimiter)) { for (int i = 0; i < str.length(); i++) { result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); } - } - else { + } else { int pos = 0; int delPos; while ((delPos = str.indexOf(delimiter, pos)) != -1) { @@ -1045,6 +1162,7 @@ public final class Strings { /** * Convert a CSV list into an array of Strings. + * * @param str the input String * @return an array of Strings, or the empty array in case of empty input */ @@ -1055,6 +1173,7 @@ public final class Strings { /** * Convenience method to convert a CSV string list to a set. * Note that this will suppress duplicates. + * * @param str the input String * @return a Set of String entries in the list */ @@ -1070,8 +1189,9 @@ public final class Strings { /** * Convenience method to return a Collection as a delimited (e.g. CSV) * String. E.g. useful for toString() implementations. - * @param coll the Collection to display - * @param delim the delimiter to use (probably a ",") + * + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") * @param prefix the String to start each element with * @param suffix the String to end each element with * @return the delimited String @@ -1094,7 +1214,8 @@ public final class Strings { /** * Convenience method to return a Collection as a delimited (e.g. CSV) * String. E.g. useful for toString() implementations. - * @param coll the Collection to display + * + * @param coll the Collection to display * @param delim the delimiter to use (probably a ",") * @return the delimited String */ @@ -1105,6 +1226,7 @@ public final class Strings { /** * Convenience method to return a Collection as a CSV String. * E.g. useful for toString() implementations. + * * @param coll the Collection to display * @return the delimited String */ @@ -1115,7 +1237,8 @@ public final class Strings { /** * Convenience method to return a String array as a delimited (e.g. CSV) * String. E.g. useful for toString() implementations. - * @param arr the array to display + * + * @param arr the array to display * @param delim the delimiter to use (probably a ",") * @return the delimited String */ @@ -1139,6 +1262,7 @@ public final class Strings { /** * Convenience method to return a String array as a CSV String. * E.g. useful for toString() implementations. + * * @param arr the array to display * @return the delimited String */ @@ -1146,5 +1270,30 @@ public final class Strings { return arrayToDelimitedString(arr, ","); } + /** + * Appends a space character (' ') if the argument is not empty, otherwise does nothing. This method + * can be thought of as "non-empty space". Using this method allows reduction of this: + *

+     * if (sb.length != 0) {
+     *     sb.append(' ');
+     * }
+     * sb.append(nextWord);
+ *

To this:

+ *
+     * nespace(sb).append(nextWord);
+ * @param sb the string builder to append a space to if non-empty + * @return the string builder argument for method chaining. + * @since JJWT_RELEASE_VERSION + */ + public static StringBuilder nespace(StringBuilder sb) { + if (sb == null) { + return null; + } + if (sb.length() != 0) { + sb.append(' '); + } + return sb; + } + } diff --git a/api/src/main/java/io/jsonwebtoken/lang/Supplier.java b/api/src/main/java/io/jsonwebtoken/lang/Supplier.java new file mode 100644 index 00000000..839a8604 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/lang/Supplier.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 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.lang; + +/** + * Represents a supplier of results. + * + *

There is no requirement that a new or distinct result be returned each time the supplier is invoked.

+ * + *

This interface is the equivalent of a JDK 8 {@code java.util.function.Supplier}, backported for JJWT's use in + * JDK 7 environments.

+ * + * @param the type of object returned by this supplier + * @since JJWT_RELEASE_VERSION + */ +public interface Supplier { + + /** + * Returns a result. + * + * @return a result. + */ + T get(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java new file mode 100644 index 00000000..a379ca80 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadAlgorithm.java @@ -0,0 +1,91 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.Jwts; + +import javax.crypto.SecretKey; + +/** + * A cryptographic algorithm that performs + *
Authenticated encryption with additional data. + * Per JWE RFC 7516, Section 4.1.2, all JWEs + * MUST use an AEAD algorithm to encrypt or decrypt the JWE payload/content. Consequently, all + * JWA "enc" algorithms are AEAD + * algorithms, and they are accessible as concrete instances via {@link Jwts#ENC}. + * + *

"enc" identifier

+ * + *

{@code AeadAlgorithm} extends {@code Identifiable}: the value returned from {@link Identifiable#getId() getId()} + * will be used as the JWE "enc" protected header value.

+ * + *

Key Strength

+ * + *

Encryption strength is in part attributed to how difficult it is to discover the encryption key. As such, + * cryptographic algorithms often require keys of a minimum length to ensure the keys are difficult to discover + * and the algorithm's security properties are maintained.

+ * + *

The {@code AeadAlgorithm} interface extends the {@link KeyLengthSupplier} interface to represent the length + * in bits a key must have to be used with its implementation. If you do not want to worry about lengths and + * parameters of keys required for an algorithm, it is often easier to automatically generate a key that adheres + * to the algorithms requirements, as discussed below.

+ * + *

Key Generation

+ * + *

{@code AeadAlgorithm} extends {@link KeyBuilderSupplier} to enable {@link SecretKey} generation. Each AEAD + * algorithm instance will return a {@link KeyBuilder} that ensures any created keys will have a sufficient length + * and algorithm parameters required by that algorithm. For example:

+ * + *

+ *     SecretKey key = aeadAlgorithm.keyBuilder().build();
+ * 
+ * + *

The resulting {@code key} is guaranteed to have the correct algorithm parameters and strength/length necessary for + * that exact {@code aeadAlgorithm} instance.

+ * + * @see Jwts#ENC + * @see Identifiable#getId() + * @see KeyLengthSupplier + * @see KeyBuilderSupplier + * @see KeyBuilder + * @since JJWT_RELEASE_VERSION + */ +public interface AeadAlgorithm extends Identifiable, KeyLengthSupplier, KeyBuilderSupplier { + + /** + * Perform AEAD encryption with the plaintext represented by the specified {@code request}, returning the + * integrity-protected encrypted ciphertext result. + * + * @param request the encryption request representing the plaintext to be encrypted, any additional + * integrity-protected data and the encryption key. + * @return the encryption result containing the ciphertext, and associated initialization vector and resulting + * authentication tag. + * @throws SecurityException if there is an encryption problem or authenticity cannot be guaranteed. + */ + AeadResult encrypt(AeadRequest request) throws SecurityException; + + /** + * Perform AEAD decryption with the ciphertext represented by the specific {@code request}, also verifying the + * integrity and authenticity of any associated data, returning the decrypted plaintext result. + * + * @param request the decryption request representing the ciphertext to be decrypted, any additional + * integrity-protected data, authentication tag, initialization vector, and the decryption key. + * @return the decryption result containing the plaintext + * @throws SecurityException if there is a decryption problem or authenticity assertions fail. + */ + Message decrypt(DecryptAeadRequest request) throws SecurityException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java new file mode 100644 index 00000000..7f981588 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadRequest.java @@ -0,0 +1,29 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * A request to an {@link AeadAlgorithm} to perform authenticated encryption with a supplied symmetric + * {@link SecretKey}, allowing for additional data to be authenticated and integrity-protected. + * + * @see SecureRequest + * @see AssociatedDataSupplier + * @since JJWT_RELEASE_VERSION + */ +public interface AeadRequest extends SecureRequest, AssociatedDataSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java new file mode 100644 index 00000000..3c0c3a04 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java @@ -0,0 +1,37 @@ +/* + * 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.security; + +/** + * The result of authenticated encryption, providing access to the resulting {@link #getPayload() ciphertext}, + * {@link #getDigest() AAD tag}, and {@link #getInitializationVector() initialization vector}. The AAD tag and + * initialization vector must be supplied with the ciphertext to decrypt. + * + *

AAD Tag

+ * + * {@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. + * + *

Initialization Vector

+ * + * 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 + */ +public interface AeadResult extends Message, DigestSupplier, InitializationVectorSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java new file mode 100644 index 00000000..3d90e8b8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java @@ -0,0 +1,37 @@ +/* + * 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.security; + +/** + * Provides any "associated data" that must be integrity protected (but not encrypted) when performing + * AEAD encryption or decryption. + * + * @see #getAssociatedData() + * @since JJWT_RELEASE_VERSION + */ +public interface AssociatedDataSupplier { + + /** + * Returns any data that must be integrity protected (but not encrypted) when performing + * AEAD encryption or decryption, or + * {@code null} if no additional data must be integrity protected. + * + * @return any data that must be integrity protected (but not encrypted) when performing + * AEAD encryption or decryption, or + * {@code null} if no additional data must be integrity protected. + */ + byte[] getAssociatedData(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java new file mode 100644 index 00000000..b6799523 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwk.java @@ -0,0 +1,75 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * JWK representation of an asymmetric (public or private) cryptographic key. + * + * @param the type of {@link java.security.PublicKey} or {@link java.security.PrivateKey} represented by this JWK. + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricJwk extends Jwk, X509Accessor { + + /** + * Returns the JWK + * {@code use} (Public Key Use) + * parameter value or {@code null} if not present. {@code use} values are CaSe-SeNsItIvE. + * + *

The JWK specification defines the + * following {@code use} values:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
JWK Key Use Values
ValueKey Use
{@code sig}signature
{@code enc}encryption
+ * + *

Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above.

+ * + *

When a key is used to wrap another key and a public key use designation for the first key is desired, the + * {@code enc} (encryption) key use value is used, since key wrapping is a kind of encryption. The + * {@code enc} value is also to be used for public keys used for key agreement operations.

+ * + *

Public Key Use vs Key Operations

+ * + *

Per + * JWK RFC 7517, Section 4.3, last paragraph, + * the {@code use} (Public Key Use) and {@link #getOperations() key_ops (Key Operations)} members + * SHOULD NOT be used together; however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they use, if either is to be used by the + * application.

+ * + * @return the JWK {@code use} value or {@code null} if not present. + */ + String getPublicKeyUse(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java new file mode 100644 index 00000000..cf106351 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/AsymmetricJwkBuilder.java @@ -0,0 +1,82 @@ +/* + * 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.security; + +import java.security.Key; +import java.util.Set; + +/** + * A {@link JwkBuilder} that builds asymmetric (public or private) JWKs. + * + * @param the type of Java key provided by the JWK. + * @param the type of asymmetric JWK created + * @param the type of the builder, for subtype method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface AsymmetricJwkBuilder, T extends AsymmetricJwkBuilder> + extends JwkBuilder, X509Builder { + + /** + * Sets the JWK + * {@code use} (Public Key Use) + * parameter value. {@code use} values are CaSe-SeNsItIvE. A {@code null} value will remove the property + * from the JWK. + * + *

The JWK specification defines the + * following {@code use} values:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
JWK Key Use Values
ValueKey Use
{@code sig}signature
{@code enc}encryption
+ * + *

Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above.

+ * + *

When a key is used to wrap another key and a public key use designation for the first key is desired, the + * {@code enc} (encryption) key use value is used, since key wrapping is a kind of encryption. The + * {@code enc} value is also to be used for public keys used for key agreement operations.

+ * + *

Public Key Use vs Key Operations

+ * + *

Per + * JWK RFC 7517, Section 4.3, last paragraph, + * the {@code use} (Public Key Use) and {@link #setOperations(Set) key_ops (Key Operations)} members + * SHOULD NOT be used together; however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they use, if either is to be used by the + * application.

+ * + * @param use the JWK {@code use} value. + * @return the builder for method chaining. + * @throws IllegalArgumentException if the {@code use} value is {@code null} or empty. + */ + T setPublicKeyUse(String use) throws IllegalArgumentException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java new file mode 100644 index 00000000..accca6a0 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptAeadRequest.java @@ -0,0 +1,28 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * A request to an {@link AeadAlgorithm} to decrypt ciphertext and perform integrity-protection with a supplied + * decryption {@link SecretKey}. Extends both {@link InitializationVectorSupplier} and {@link DigestSupplier} to + * ensure the respective required IV and AAD tag returned from an {@link AeadResult} are available for decryption. + * + * @since JJWT_RELEASE_VERSION + */ +public interface DecryptAeadRequest extends AeadRequest, InitializationVectorSupplier, DigestSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java new file mode 100644 index 00000000..3a4b84c7 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/DecryptionKeyRequest.java @@ -0,0 +1,42 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * A {@link KeyRequest} to obtain a decryption key that will be used to decrypt a JWE using an {@link AeadAlgorithm}. + * The AEAD algorithm used for decryption is accessible via {@link #getEncryptionAlgorithm()}. + * + *

The key used to perform cryptographic operations, for example a direct shared key, or a + * JWE "key decryption key" will be accessible via {@link #getKey()}. This is always required and + * never {@code null}.

+ * + *

Any encrypted key material (what the JWE specification calls the + * JWE Encrypted Key) will + * be accessible via {@link #getPayload()}. If present, the {@link KeyAlgorithm} will decrypt it to obtain the resulting + * Content Encryption Key (CEK). + * This may be empty however depending on which {@link KeyAlgorithm} was used during JWE encryption.

+ * + *

Finally, any public information necessary by the called {@link KeyAlgorithm} to decrypt any + * {@code JWE Encrypted Key} (such as an initialization vector, authentication tag, ephemeral key, etc) is expected + * to be available in the JWE protected header, accessible via {@link #getHeader()}.

+ * + * @param the type of {@link Key} used during the request to obtain the resulting decryption key. + * @since JJWT_RELEASE_VERSION + */ +public interface DecryptionKeyRequest extends SecureRequest, KeyRequest { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/DigestAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/DigestAlgorithm.java new file mode 100644 index 00000000..8a5a8cf2 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/DigestAlgorithm.java @@ -0,0 +1,100 @@ +/* + * Copyright © 2022 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.Registry; + +import javax.crypto.SecretKey; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * A {@code DigestAlgorithm} is a + * Cryptographic Hash Function + * that computes and verifies cryptographic digests. There are three types of {@code DigestAlgorithm}s represented + * by subtypes, and RFC-standard implementations are available as constants in {@link Registry} singletons: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Types of {@code DigestAlgorithm}s
SubtypeStandard Implementation RegistrySecurity Model
{@link HashAlgorithm}{@link StandardHashAlgorithms}Unsecured (unkeyed), does not require a key to compute or verify digests.
{@link MacAlgorithm}{@link StandardSecureDigestAlgorithms}Requires a {@link SecretKey} to both compute and verify digests (aka + * "Message Authentication Codes").
{@link SignatureAlgorithm}{@link StandardSecureDigestAlgorithms}Requires a {@link PrivateKey} to compute and {@link PublicKey} to verify digests + * (aka "Digital Signatures").
+ * + *

Standard Identifier

+ * + *

{@code DigestAlgorithm} extends {@link Identifiable}: the value returned from + * {@link Identifiable#getId() getId()} will be used as the JWT standard identifier where required.

+ * + *

For example, + * when a {@link MacAlgorithm} or {@link SignatureAlgorithm} is used to secure a JWS, the value returned from + * {@code algorithm.getId()} will be used as the JWS "alg" protected header value. Or when a + * {@link HashAlgorithm} is used to compute a {@link JwkThumbprint}, it's {@code algorithm.getId()} value will be + * used within the thumbprint's {@link JwkThumbprint#toURI() URI} per JWT RFC requirements.

+ * + * @param the type of {@link Request} used when computing a digest. + * @param the type of {@link VerifyDigestRequest} used when verifying a digest. + * @see StandardHashAlgorithms + * @see StandardSecureDigestAlgorithms + * @since JJWT_RELEASE_VERSION + */ +public interface DigestAlgorithm, V extends VerifyDigestRequest> extends Identifiable { + + /** + * Returns a cryptographic digest of the request {@link Request#getPayload() payload}. + * + * @param request the request containing the data to be hashed, mac'd or signed. + * @return a cryptographic digest of the request {@link Request#getPayload() payload}. + * @throws SecurityException if there is invalid key input or a problem during digest creation. + */ + byte[] digest(R request) throws SecurityException; + + /** + * Returns {@code true} if the provided {@link VerifyDigestRequest#getDigest() digest} matches the expected value + * for the given {@link VerifyDigestRequest#getPayload() payload}, {@code false} otherwise. + * + * @param request the request containing the {@link VerifyDigestRequest#getDigest() digest} to verify for the + * associated {@link VerifyDigestRequest#getPayload() payload}. + * @return {@code true} if the provided {@link VerifyDigestRequest#getDigest() digest} matches the expected value + * for the given {@link VerifyDigestRequest#getPayload() payload}, {@code false} otherwise. + * @throws SecurityException if there is an invalid key input or a problem that won't allow digest verification. + */ + boolean verify(V request) throws SecurityException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java new file mode 100644 index 00000000..60f0ec2b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java @@ -0,0 +1,35 @@ +/* + * 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.security; + +/** + * A {@code DigestSupplier} provides access to the result of a cryptographic digest algorithm, such as a + * Message Digest, MAC, Signature, or Authentication Tag. + * + * @since JJWT_RELEASE_VERSION + */ +public interface DigestSupplier { + + /** + * Returns a cryptographic digest result, such as a Message Digest, MAC, Signature, or Authentication Tag + * depending on the cryptographic algorithm that produced it. + * + * @return a cryptographic digest result, such as a Message Digest, MAC, Signature, or Authentication Tag + * * depending on the cryptographic algorithm that produced it. + */ + byte[] getDigest(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java new file mode 100644 index 00000000..7d2a2714 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwk.java @@ -0,0 +1,43 @@ +/* + * 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.security; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * JWK representation of an {@link ECPrivateKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for Elliptic Curve Keys and + * Parameters for Elliptic Curve Private Keys. + * + *

Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link ECPrivateKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + *

Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

+ *
+ * jwk.get("x");
+ * jwk.get("y");
+ * // ... etc ...
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface EcPrivateJwk extends PrivateJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java new file mode 100644 index 00000000..07d91f44 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPrivateJwkBuilder.java @@ -0,0 +1,27 @@ +/* + * 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.security; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * A {@link PrivateJwkBuilder} that creates {@link EcPrivateJwk}s. + * + * @since JJWT_RELEASE_VERSION + */ +public interface EcPrivateJwkBuilder extends PrivateJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java new file mode 100644 index 00000000..925f61f7 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwk.java @@ -0,0 +1,42 @@ +/* + * 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.security; + +import java.security.interfaces.ECPublicKey; + +/** + * JWK representation of an {@link ECPublicKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for Elliptic Curve Keys and + * Parameters for Elliptic Curve Public Keys. + * + *

Note that the various EC-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link ECPublicKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + *

Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

+ *
+ * jwk.get("x");
+ * jwk.get("y");
+ * // ... etc ...
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface EcPublicJwk extends PublicJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java new file mode 100644 index 00000000..4a0ce887 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcPublicJwkBuilder.java @@ -0,0 +1,27 @@ +/* + * 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.security; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * A {@link PublicJwkBuilder} that creates {@link EcPublicJwk}s. + * + * @since JJWT_RELEASE_VERSION + */ +public interface EcPublicJwkBuilder extends PublicJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java new file mode 100644 index 00000000..5e60edad --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2022 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.security; + +import io.jsonwebtoken.Identifiable; + +/** + * A {@link DigestAlgorithm} that computes and verifies digests without the use of a cryptographic key, such as for + * thumbprints and digital fingerprints. + * + *

Standard Identifier

+ * + *

{@code HashAlgorithm} extends {@link Identifiable}: the value returned from + * {@link Identifiable#getId() getId()} in all JWT standard hash algorithms will return one of the + * "{@code Hash Name String}" values defined in the IANA + * Named Information Hash + * Algorithm Registry. This is to ensure the correct algorithm ID is used within other JWT-standard identifiers, + * such as within JWK Thumbprint URIs.

+ * + *

IANA Standard Implementations

+ * + *

Constant definitions and utility methods for common (but not all) + * IANA Hash + * Algorithms are available via the {@link StandardHashAlgorithms} singleton.

+ * + * @see StandardHashAlgorithms + * @since JJWT_RELEASE_VERSION + */ +public interface HashAlgorithm extends DigestAlgorithm, VerifyDigestRequest> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java new file mode 100644 index 00000000..22294e5a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java @@ -0,0 +1,36 @@ +/* + * 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.security; + +/** + * An {@code InitializationVectorSupplier} provides access to the secure-random Initialization Vector used during + * encryption, which must in turn be presented for use during decryption. To maintain the security integrity of cryptographic + * algorithms, a new secure-random Initialization Vector MUST be generated for every individual + * encryption attempt. + * + * @since JJWT_RELEASE_VERSION + */ +public interface InitializationVectorSupplier { + + /** + * Returns the secure-random Initialization Vector used during encryption, which must in turn be presented for + * use during decryption. + * + * @return the secure-random Initialization Vector used during encryption, which must in turn be presented for + * use during decryption. + */ + byte[] getInitializationVector(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java index 2e3b84b8..ad9d4a7d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java @@ -16,11 +16,30 @@ package io.jsonwebtoken.security; /** + * A {@code KeyException} thrown when encountering a key that is not suitable for the required functionality, or + * when attempting to use a Key in an incorrect or prohibited manner. + * * @since 0.10.0 */ public class InvalidKeyException extends KeyException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public InvalidKeyException(String message) { super(message); } + + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + * @since JJWT_RELEASE_VERSION + */ + public InvalidKeyException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwk.java b/api/src/main/java/io/jsonwebtoken/security/Jwk.java new file mode 100644 index 00000000..7f3a947a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Jwk.java @@ -0,0 +1,226 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.Supplier; + +import java.security.Key; +import java.util.Map; +import java.util.Set; + +/** + * A JWK is an immutable set of name/value pairs that represent a cryptographic key as defined by + * RFC 7517: JSON Web Key (JWK). The {@code Jwk} + * interface represents properties common to all JWKs. Subtypes will have additional properties specific to + * different types of cryptographic keys (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc). + * + *

Immutability

+ * + *

JWKs are immutable and cannot be changed after they are created. {@code Jwk} extends the + * {@link Map} interface purely out of convenience: to allow easy marshalling to JSON as well as name/value + * pair access and key/value iteration, and other conveniences provided by the Map interface. Attempting to call any of + * the {@link Map} interface's mutation methods however (such as {@link Map#put(Object, Object) put}, + * {@link Map#remove(Object) remove}, {@link Map#clear() clear}, etc) will throw an + * {@link UnsupportedOperationException}.

+ * + *

Identification

+ * + *

{@code Jwk} extends {@link Identifiable} to support the + * JWK {@code kid} parameter. Calling + * {@link #getId() aJwk.getId()} is the type-safe idiomatic approach to the alternative equivalent of + * {@code aJwk.get("kid")}. Either approach will return an id if one was originally set on the JWK, or {@code null} if + * an id does not exist.

+ * + *

Private and Secret Value Safety

+ * + *

JWKs often represent secret or private key data which should never be exposed publicly, nor mistakenly printed + * to application logs or {@code System.out.println} calls. As a result, all JJWT JWK + * private or secret field values are 'wrapped' in a {@link io.jsonwebtoken.lang.Supplier Supplier} instance to ensure + * any attempt to call {@link String#toString() toString()} on the value will print a redacted value instead of an + * actual private or secret value.

+ * + *

For example, a {@link SecretJwk} will have an internal "{@code k}" member whose value reflects raw + * key material that should always be kept secret. If the following is called:

+ *
+ * System.out.println(aSecretJwk.get("k"));
+ *

You would see the following:

+ *
+ * <redacted>
+ *

instead of the actual/raw {@code k} value.

+ * + *

Similarly, if attempting to print the entire JWK:

+ *
+ * System.out.println(aSecretJwk);
+ *

You would see the following substring in the output:

+ *
+ * k=<redacted>
+ *

instead of the actual/raw {@code k} value.

+ * + *

Finally, because all private or secret field values are wrapped as {@link io.jsonwebtoken.lang.Supplier} + * instances, if you really wanted the real internal value, you could just call the supplier's + * {@link Supplier#get() get()} method:

+ *
+ * String k = ((Supplier<String>)aSecretJwk.get("k")).get();
+ *

but BE CAREFUL: obtaining the raw value in your application code exposes greater security + * risk - you must ensure to keep that value safe and out of console or log output. It is almost always better to + * interact with the JWK's {@link #toKey() toKey()} instance directly instead of accessing + * JWK internal serialization fields.

+ * + * @param The type of Java {@link Key} represented by this JWK + * @since JJWT_RELEASE_VERSION + */ +public interface Jwk extends Identifiable, Map { + + /** + * Returns the JWK + * {@code alg} (Algorithm) value + * or {@code null} if not present. + * + * @return the JWK {@code alg} value or {@code null} if not present. + */ + String getAlgorithm(); + + /** + * Returns the JWK + * {@code key_ops} (Key Operations) + * parameter values or {@code null} if not present. Any values within the returned {@code Set} are + * CaSe-SeNsItIvE. + * + *

The JWK specification defines the + * following values:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
JWK Key Operations
ValueOperation
{@code sign}compute digital signatures or MAC
{@code verify}verify digital signatures or MAC
{@code encrypt}encrypt content
{@code decrypt}decrypt content and validate decryption, if applicable
{@code wrapKey}encrypt key
{@code unwrapKey}decrypt key and validate decryption, if applicable
{@code deriveKey}derive key
{@code deriveBits}derive bits not to be used as a key
+ * + *

Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above.

+ * + *

Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential + * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations + * {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with + * {@code unwrapKey} are permitted, but other combinations SHOULD NOT be used.

+ * + * @return the JWK {@code key_ops} value or {@code null} if not present. + */ + Set getOperations(); + + /** + * Returns the required JWK + * {@code kty} (Key Type) + * parameter value. A value is required and may not be {@code null}. + * + *

The JWA specification defines the + * following {@code kty} values:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
JWK Key Types
ValueKey Type
{@code EC}Elliptic Curve [DSS]
{@code RSA}RSA [RFC 3447]
{@code oct}Octet sequence (used to represent symmetric keys)
+ * + * @return the JWK {@code kty} (Key Type) value. + */ + String getType(); + + /** + * Computes and returns the canonical JWK Thumbprint of this + * JWK using the {@code SHA-256} hash algorithm. This is a convenience method that delegates to + * {@link #thumbprint(HashAlgorithm)} with a {@code SHA-256} {@link HashAlgorithm} instance. + * + * @return the canonical JWK Thumbprint of this + * JWK using the {@code SHA-256} hash algorithm. + * @see #thumbprint(HashAlgorithm) + */ + JwkThumbprint thumbprint(); + + /** + * Computes and returns the canonical JWK Thumbprint of this + * JWK using the specified hash algorithm. + * + * @param alg the hash algorithm to use to compute the digest of the canonical JWK Thumbprint JSON form of this JWK. + * @return the canonical JWK Thumbprint of this + * JWK using the specified hash algorithm. + */ + JwkThumbprint thumbprint(HashAlgorithm alg); + + /** + * Represents the JWK as its corresponding Java {@link Key} instance for use with Java cryptographic + * APIs. + * + * @return the JWK's corresponding Java {@link Key} instance for use with Java cryptographic APIs. + */ + K toKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java new file mode 100644 index 00000000..48d109e6 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java @@ -0,0 +1,178 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.MapMutator; + +import java.security.Key; +import java.util.Set; + +/** + * A {@link SecurityBuilder} that produces a JWK. A JWK is an immutable set of name/value pairs that represent a + * cryptographic key as defined by + * RFC 7517: JSON Web Key (JWK). + * The {@code JwkBuilder} interface represents common JWK properties that may be specified for any type of JWK. + * Builder subtypes support additional JWK properties specific to different types of cryptographic keys + * (e.g. Secret, Asymmetric, RSA, Elliptic Curve, etc). + * + * @param the type of Java {@link Key} represented by the constructed JWK. + * @param the type of {@link Jwk} created by the builder + * @param the type of the builder, for subtype method chaining + * @see SecretJwkBuilder + * @see RsaPublicJwkBuilder + * @see RsaPrivateJwkBuilder + * @see EcPublicJwkBuilder + * @see EcPrivateJwkBuilder + * @see OctetPublicJwkBuilder + * @see OctetPrivateJwkBuilder + * @since JJWT_RELEASE_VERSION + */ +public interface JwkBuilder, T extends JwkBuilder> + extends MapMutator, SecurityBuilder { + + /** + * Sets the JWK {@code alg} (Algorithm) + * Parameter. + * + *

The {@code alg} (algorithm) parameter identifies the algorithm intended for use with the key. The + * value specified should either be one of the values in the IANA + * JSON Web Signature and Encryption + * Algorithms registry or be a value that contains a {@code Collision-Resistant Name}. The {@code alg} + * must be a CaSe-SeNsItIvE ASCII string.

+ * + * @param alg the JWK {@code alg} value. + * @return the builder for method chaining. + * @throws IllegalArgumentException if {@code alg} is {@code null} or empty. + */ + T setAlgorithm(String alg) throws IllegalArgumentException; + + /** + * Sets the JWK {@code kid} (Key ID) + * Parameter. + * + *

The {@code kid} (key ID) parameter is used to match a specific key. This is used, for instance, + * to choose among a set of keys within a {@code JWK Set} during key rollover. The structure of the + * {@code kid} value is unspecified. When {@code kid} values are used within a JWK Set, different keys + * within the {@code JWK Set} SHOULD use distinct {@code kid} values. (One example in which + * different keys might use the same {@code kid} value is if they have different {@code kty} (key type) + * values but are considered to be equivalent alternatives by the application using them.)

+ * + *

The {@code kid} value is a CaSe-SeNsItIvE string, and it is optional. When used with JWS or JWE, + * the {@code kid} value is used to match a JWS or JWE {@code kid} Header Parameter value.

+ * + * @param kid the JWK {@code kid} value. + * @return the builder for method chaining. + * @throws IllegalArgumentException if the argument is {@code null} or empty. + */ + T setId(String kid) throws IllegalArgumentException; + + /** + * Sets the JWK's {@link #setId(String) kid} value to be the Base64URL-encoding of its {@code SHA-256} + * {@link Jwk#thumbprint(HashAlgorithm) thumbprint}. That is, the constructed JWK's {@code kid} value will equal + * jwk.{@link Jwk#thumbprint(HashAlgorithm) thumbprint}({@link Jwks#HASH}.{@link StandardHashAlgorithms#SHA256 SHA256}).{@link JwkThumbprint#toString() toString()}. + * + *

This is a convenience method that delegates to {@link #setIdFromThumbprint(HashAlgorithm)} using + * {@link Jwks#HASH}{@code .}{@link StandardHashAlgorithms#SHA256 SHA256}.

+ * + * @return the builder for method chaining. + */ + T setIdFromThumbprint(); + + /** + * Sets the JWK's {@link #setId(String) kid} value to be the Base64URL-encoding of its + * {@link Jwk#thumbprint(HashAlgorithm) thumbprint} using the specified {@link HashAlgorithm}. That is, the + * constructed JWK's {@code kid} value will equal + * {@link Jwk#thumbprint(HashAlgorithm) thumbprint}(alg).{@link JwkThumbprint#toString() toString()}. + * + * @param alg the hash algorithm to use to compute the thumbprint. + * @return the builder for method chaining. + * @see StandardHashAlgorithms + */ + T setIdFromThumbprint(HashAlgorithm alg); + + /** + * Sets the JWK {@code key_ops} + * (Key Operations) Parameter values. + * + *

The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is + * intended to be used. The {@code key_ops} parameter is intended for use cases in which public, + * private, or symmetric keys may be present.

+ * + *

The JWK specification defines the + * following values:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
JWK Key Operations
ValueOperation
{@code sign}compute digital signatures or MAC
{@code verify}verify digital signatures or MAC
{@code encrypt}encrypt content
{@code decrypt}decrypt content and validate decryption, if applicable
{@code wrapKey}encrypt key
{@code unwrapKey}decrypt key and validate decryption, if applicable
{@code deriveKey}derive key
{@code deriveBits}derive bits not to be used as a key
+ * + *

(Note that {@code key_ops} values intentionally match the {@code KeyUsage} values defined in the + * Web Cryptography API specification.)

+ * + *

Other values MAY be used. For best interoperability with other applications however, it is + * recommended to use only the values above. Each value is a CaSe-SeNsItIvE string. Use of the + * {@code key_ops} member is OPTIONAL, unless the application requires its presence.

+ * + *

Multiple unrelated key operations SHOULD NOT be specified for a key because of the potential + * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations + * {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with + * {@code unwrapKey} are permitted, but other combinations SHOULD NOT be used.

+ * + * @param ops the JWK {@code key_ops} value set. + * @return the builder for method chaining. + * @throws IllegalArgumentException if {@code ops} is {@code null} or empty. + */ + T setOperations(Set ops) throws IllegalArgumentException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkParser.java b/api/src/main/java/io/jsonwebtoken/security/JwkParser.java new file mode 100644 index 00000000..f4d471ab --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkParser.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 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.security; + +/** + * Parses a JWK JSON string and produces its resulting {@link Jwk} instance. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JwkParser { + + /** + * Parses the specified JWK JSON string and returns the resulting {@link Jwk} instance. + * + * @param json the json string representing the JWK + * @return the {@link Jwk} instance corresponding to the specified JWK json string. + * @throws KeyException if the json string cannot be represented as a {@link Jwk}. + */ + Jwk parse(String json) throws KeyException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java b/api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java new file mode 100644 index 00000000..7dd07f5e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkParserBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Builder; + +import java.security.Provider; +import java.util.Map; + +/** + * A builder to construct a {@link JwkParser}. Example usage: + *
+ * Jwk<?> jwk = Jwks.parser()
+ *         .setProvider(aJcaProvider)         // optional
+ *         .deserializeJsonWith(deserializer) // optional
+ *         .build()
+ *         .parse(jwkString);
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface JwkParserBuilder extends Builder { + + /** + * Sets the JCA Provider to use during cryptographic key factory operations, or {@code null} if the + * JCA subsystem preferred provider should be used. + * + * @param provider the JCA Provider to use during cryptographic key factory operations, or {@code null} + * if the JCA subsystem preferred provider should be used. + * @return the builder for method chaining. + */ + JwkParserBuilder setProvider(Provider provider); + + /** + * Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. The + * resulting Maps are then used to construct {@link Jwk} instances. + * + *

If this method is not called, JJWT will use whatever deserializer it can find at runtime, checking for the + * presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found + * in the runtime classpath, an exception will be thrown when the resulting {@link JwkParser}'s + * {@link JwkParser#parse(String) parse(json)} method is called. + * + * @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects. + * @return the builder for method chaining. + */ + JwkParserBuilder deserializeJsonWith(Deserializer> deserializer); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/JwkThumbprint.java b/api/src/main/java/io/jsonwebtoken/security/JwkThumbprint.java new file mode 100644 index 00000000..c88fca3f --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/JwkThumbprint.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2022 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.security; + +import java.net.URI; + +/** + * A canonical cryptographic digest of a JWK as defined by the + * JSON Web Key (JWK) Thumbprint specification. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JwkThumbprint { + + /** + * Returns the {@link HashAlgorithm} used to compute the thumbprint. + * + * @return the {@link HashAlgorithm} used to compute the thumbprint. + */ + HashAlgorithm getHashAlgorithm(); + + /** + * Returns the actual thumbprint (aka digest) byte array value. + * + * @return the actual thumbprint (aka digest) byte array value. + */ + byte[] toByteArray(); + + /** + * Returns the canonical URI representation of this thumbprint as defined by the + * JWK Thumbprint URI specification. + * + * @return a canonical JWK Thumbprint URI + */ + URI toURI(); + + /** + * Returns the {@link #toByteArray()} value as a Base64URL-encoded string. + */ + String toString(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Jwks.java b/api/src/main/java/io/jsonwebtoken/security/Jwks.java new file mode 100644 index 00000000..053f2b82 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Jwks.java @@ -0,0 +1,86 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Classes; + +/** + * Utility methods for creating + * JWKs (JSON Web Keys) with a type-safe builder. + * + *

Standard JWK Thumbprint Algorithm References

+ *

Standard IANA Hash + * Algorithms commonly used to compute {@link JwkThumbprint JWK Thumbprint}s and ensure valid + * JWK Thumbprint URIs + * are available via the {@link #HASH} registry constant to allow for easy code-completion in IDEs. For example, when + * typing:

+ *
+ * Jwts.{@link #HASH}.// press hotkeys to suggest individual hash algorithms or utility methods
+ * + * @see #builder() + * @since JJWT_RELEASE_VERSION + */ +public final class Jwks { + + private Jwks() { + } //prevent instantiation + + private static final String BUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultProtoJwkBuilder"; + + private static final String PARSERBUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder"; + + /** + * Registry of various (but not all) + * IANA Hash + * Algorithms commonly used to compute {@link JwkThumbprint JWK Thumbprint}s and ensure valid + * JWK Thumbprint URIs. For + * example: + *
+     * Jwks.{@link Jwks#builder}()
+     *     // ... etc ...
+     *     .{@link JwkBuilder#setIdFromThumbprint(HashAlgorithm) setIdFromThumbprint}(Jwts.HASH.{@link StandardHashAlgorithms#SHA256 SHA256}) // <---
+     *     .build()
+ *

or

+ *
+     * HashAlgorithm hashAlg = Jwks.HASH.{@link StandardHashAlgorithms#SHA256 SHA256};
+     * {@link JwkThumbprint} thumbprint = aJwk.{@link Jwk#thumbprint(HashAlgorithm) thumbprint}(hashAlg);
+     * String rfcMandatoryPrefix = "urn:ietf:params:oauth:jwk-thumbprint:" + hashAlg.getId();
+     * assert thumbprint.toURI().toString().startsWith(rfcMandatoryPrefix);
+     * 
+ * + * @since JJWT_RELEASE_VERSION + */ + public static final StandardHashAlgorithms HASH = StandardHashAlgorithms.get(); + + /** + * Return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. + * + * @return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. + */ + public static ProtoJwkBuilder builder() { + return Classes.newInstance(BUILDER_CLASSNAME); + } + + /** + * Return a new thread-safe {@link JwkParserBuilder} to parse JSON strings into {@link Jwk} instances. + * + * @return a new thread-safe {@link JwkParserBuilder} to parse JSON strings into {@link Jwk} instances. + */ + public static JwkParserBuilder parser() { + return Classes.newInstance(PARSERBUILDER_CLASSNAME); + } + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java new file mode 100644 index 00000000..90be4fc2 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java @@ -0,0 +1,84 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.Jwts; + +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * A {@code KeyAlgorithm} produces the {@link SecretKey} used to encrypt or decrypt a JWE. The {@code KeyAlgorithm} + * used for a particular JWE is {@link #getId() identified} in the JWE's + * {@code alg} header. The {@code KeyAlgorithm} + * interface is JJWT's idiomatic approach to the JWE specification's + * {@code Key Management Mode} concept. + * + *

All standard Key Algorithms are defined in + * JWA (RFC 7518), Section 4.1, + * and they are all available as concrete instances via {@link Jwts#KEY}.

+ * + *

"alg" identifier

+ * + *

{@code KeyAlgorithm} extends {@code Identifiable}: the value returned from + * {@link Identifiable#getId() keyAlgorithm.getId()} will be used as the + * JWE "alg" protected header value.

+ * + * @param The type of key to use to obtain the AEAD encryption key + * @param The type of key to use to obtain the AEAD decryption key + * @see Jwts#KEY + * @see RFC 7561, Section 2: JWE Key (Management) Algorithms + * @since JJWT_RELEASE_VERSION + */ +@SuppressWarnings("JavadocLinkAsPlainText") +public interface KeyAlgorithm extends Identifiable { + + /** + * Return the {@link SecretKey} that should be used to encrypt a JWE via the request's specified + * {@link KeyRequest#getEncryptionAlgorithm() AeadAlgorithm}. The encryption key will + * be available via the result's {@link KeyResult#getKey() result.getKey()} method. + * + *

If the key algorithm uses key encryption or key agreement to produce an encrypted key value that must be + * included in the JWE, the encrypted key ciphertext will be available via the result's + * {@link KeyResult#getPayload() result.getPayload()} method. If the key algorithm does not produce encrypted + * key ciphertext, {@link KeyResult#getPayload() result.getPayload()} will be a non-null empty byte array.

+ * + * @param request the {@code KeyRequest} containing information necessary to produce a {@code SecretKey} for + * {@link AeadAlgorithm AEAD} encryption. + * @return the {@link SecretKey} that should be used to encrypt a JWE via the request's specified + * {@link KeyRequest#getEncryptionAlgorithm() AeadAlgorithm}, along with any optional encrypted key ciphertext. + * @throws SecurityException if there is a problem obtaining or encrypting the AEAD {@code SecretKey}. + */ + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException; + + /** + * Return the {@link SecretKey} that should be used to decrypt a JWE via the request's specified + * {@link DecryptionKeyRequest#getEncryptionAlgorithm() AeadAlgorithm}. + * + *

If the key algorithm used key encryption or key agreement to produce an encrypted key value, the encrypted + * key ciphertext will be available via the request's {@link DecryptionKeyRequest#getPayload() result.getPayload()} + * method. If the key algorithm did not produce encrypted key ciphertext, + * {@link DecryptionKeyRequest#getPayload() request.getPayload()} will return a non-null empty byte array.

+ * + * @param request the {@code DecryptionKeyRequest} containing information necessary to obtain a + * {@code SecretKey} for {@link AeadAlgorithm AEAD} decryption. + * @return the {@link SecretKey} that should be used to decrypt a JWE via the request's specified + * {@link DecryptionKeyRequest#getEncryptionAlgorithm() AeadAlgorithm}. + * @throws SecurityException if there is a problem obtaining or decrypting the AEAD {@code SecretKey}. + */ + SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java new file mode 100644 index 00000000..dd50239a --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilder.java @@ -0,0 +1,34 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * A {@code KeyBuilder} produces new {@link Key}s suitable for use with an associated cryptographic algorithm. + * A new {@link Key} is created each time the builder's {@link #build()} method is called. + * + *

{@code KeyBuilder}s are provided by components that implement the {@link KeyBuilderSupplier} interface, + * ensuring the resulting {@link SecretKey}s are compatible with their associated cryptographic algorithm.

+ * + * @param the type of key to build + * @param the type of the builder, for subtype method chaining + * @see KeyBuilderSupplier + * @since JJWT_RELEASE_VERSION + */ +public interface KeyBuilder> extends SecurityBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java new file mode 100644 index 00000000..29c1e53d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyBuilderSupplier.java @@ -0,0 +1,40 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * Interface implemented by components that support building/creating new {@link Key}s suitable for use with + * their associated cryptographic algorithm implementation. + * + * @param type of {@link Key} created by the builder + * @param type of builder to create each time {@link #keyBuilder()} is called. + * @see #keyBuilder() + * @see KeyBuilder + * @since JJWT_RELEASE_VERSION + */ +public interface KeyBuilderSupplier> { + + /** + * Returns a new {@link KeyBuilder} instance that will produce new secure-random keys with a length sufficient + * to be used by the component's associated cryptographic algorithm. + * + * @return a new {@link KeyBuilder} instance that will produce new secure-random keys with a length sufficient + * to be used by the component's associated cryptographic algorithm. + */ + B keyBuilder(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyException.java b/api/src/main/java/io/jsonwebtoken/security/KeyException.java index 0db21924..4deb8661 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyException.java @@ -16,11 +16,29 @@ package io.jsonwebtoken.security; /** + * General-purpose exception when encountering a problem with a cryptographic {@link java.security.Key} + * or {@link Jwk}. + * * @since 0.10.0 */ public class KeyException extends SecurityException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public KeyException(String message) { super(message); } + + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param msg the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public KeyException(String msg, Throwable cause) { + super(msg, cause); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java new file mode 100644 index 00000000..40723028 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyLengthSupplier.java @@ -0,0 +1,31 @@ +/* + * 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.security; + +/** + * Provides access to the required length in bits (not bytes) of keys usable with the associated algorithm. + * + * @since JJWT_RELEASE_VERSION + */ +public interface KeyLengthSupplier { + + /** + * Returns the required length in bits (not bytes) of keys usable with the associated algorithm. + * + * @return the required length in bits (not bytes) of keys usable with the associated algorithm. + */ + int getKeyBitLength(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPair.java b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java new file mode 100644 index 00000000..f680534b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPair.java @@ -0,0 +1,51 @@ +/* + * 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Generics-capable and type-safe alternative to {@link java.security.KeyPair}. Instances may be + * converted to {@link java.security.KeyPair} if desired via {@link #toJavaKeyPair()}. + * + * @param The type of {@link PublicKey} in the key pair. + * @param The type of {@link PrivateKey} in the key pair. + * @since JJWT_RELEASE_VERSION + */ +public interface KeyPair { + + /** + * Returns the pair's public key. + * + * @return the pair's public key. + */ + A getPublic(); + + /** + * Returns the pair's private key. + * + * @return the pair's private key. + */ + B getPrivate(); + + /** + * Returns this instance as a {@link java.security.KeyPair} instance. + * + * @return this instance as a {@link java.security.KeyPair} instance. + */ + java.security.KeyPair toJavaKeyPair(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java new file mode 100644 index 00000000..2b240609 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilder.java @@ -0,0 +1,31 @@ +/* + * 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.security; + +import java.security.KeyPair; + +/** + * A {@code KeyPairBuilder} produces new {@link KeyPair}s suitable for use with an associated cryptographic algorithm. + * A new {@link KeyPair} is created each time the builder's {@link #build()} method is called. + * + *

{@code KeyPairBuilder}s are provided by components that implement the {@link KeyPairBuilderSupplier} interface, + * ensuring the resulting {@link KeyPair}s are compatible with their associated cryptographic algorithm.

+ * + * @see KeyPairBuilderSupplier + * @since JJWT_RELEASE_VERSION + */ +public interface KeyPairBuilder extends SecurityBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java new file mode 100644 index 00000000..a93bc576 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyPairBuilderSupplier.java @@ -0,0 +1,38 @@ +/* + * 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.security; + +import java.security.KeyPair; + +/** + * Interface implemented by components that support building/creating new {@link KeyPair}s suitable for use with their + * associated cryptographic algorithm implementation. + * + * @see #keyPairBuilder() + * @see KeyPairBuilder + * @since JJWT_RELEASE_VERSION + */ +public interface KeyPairBuilderSupplier { + + /** + * Returns a new {@link KeyPairBuilder} that will create new secure-random {@link KeyPair}s with a length and + * parameters sufficient for use with the component's associated cryptographic algorithm. + * + * @return a new {@link KeyPairBuilder} that will create new secure-random {@link KeyPair}s with a length and + * parameters sufficient for use with the component's associated cryptographic algorithm. + */ + KeyPairBuilder keyPairBuilder(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java new file mode 100644 index 00000000..747b618d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java @@ -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.security; + +import io.jsonwebtoken.JweHeader; + +/** + * A request to a {@link KeyAlgorithm} to obtain the key necessary for AEAD encryption or decryption. The exact + * {@link AeadAlgorithm} that will be used is accessible via {@link #getEncryptionAlgorithm()}. + * + *

Encryption Requests

+ *

For an encryption key request, {@link #getPayload()} will return + * the encryption key to use. Additionally, any public information specific to the called + * {@link KeyAlgorithm} implementation that is required to be transmitted in the JWE (such as an initialization vector, + * authentication tag or ephemeral key, etc) may be added to the JWE protected header, accessible via + * {@link #getHeader()}. Although the JWE header is checked for authenticity and integrity, it itself is + * not encrypted, so {@link KeyAlgorithm}s should never place any secret or private information in the + * header.

+ * + *

Decryption Requests

+ *

For a decryption request, the {@code KeyRequest} instance will be + * a {@link DecryptionKeyRequest} instance, {@link #getPayload()} will return the encrypted key ciphertext (a + * byte array), and the decryption key will be available via {@link DecryptionKeyRequest#getKey()}. Additionally, + * any public information necessary by the called {@link KeyAlgorithm} (such as an initialization vector, + * authentication tag, ephemeral key, etc) is expected to be available in the JWE protected header, accessible + * via {@link #getHeader()}.

+ * + * @param the type of object relevant during key algorithm cryptographic operations. + * @see DecryptionKeyRequest + * @since JJWT_RELEASE_VERSION + */ +public interface KeyRequest extends Request { + + /** + * Returns the {@link AeadAlgorithm} that will be called for encryption or decryption after processing the + * {@code KeyRequest}. {@link KeyAlgorithm} implementations that generate an ephemeral {@code SecretKey} to use + * as what the
JWE specification calls a + * "Content Encryption Key (CEK)" should call the {@code AeadAlgorithm}'s + * {@link AeadAlgorithm#keyBuilder() keyBuilder()} to obtain a builder that will create a key suitable for that + * exact {@code AeadAlgorithm}. + * + * @return the {@link AeadAlgorithm} that will be called for encryption or decryption after processing the + * {@code KeyRequest}. + */ + AeadAlgorithm getEncryptionAlgorithm(); + + /** + * Returns the {@link JweHeader} that will be used to construct the final JWE, available for reading or writing + * any {@link KeyAlgorithm}-specific information. + * + *

For an encryption key request, any public information specific to the called {@code KeyAlgorithm} + * implementation that is required to be transmitted in the JWE (such as an initialization vector, + * authentication tag or ephemeral key, etc) is expected to be added to this header. Although the header is + * checked for authenticity and integrity, it itself is not encrypted, so + * {@link KeyAlgorithm}s should never place any secret or private information in the header.

+ * + *

For a decryption request, any public information necessary by the called {@link KeyAlgorithm} + * (such as an initialization vector, authentication tag, ephemeral key, etc) is expected to be available in + * this header.

+ * + * @return the {@link JweHeader} that will be used to construct the final JWE, available for reading or writing + * any {@link KeyAlgorithm}-specific information. + */ + JweHeader getHeader(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java new file mode 100644 index 00000000..5c029fb8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java @@ -0,0 +1,34 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * The result of a {@link KeyAlgorithm} encryption key request, containing the resulting + * {@code JWE encrypted key} and {@code JWE Content Encryption Key (CEK)}, concepts defined in + * JWE Terminology. + * + *

The result {@link #getPayload() payload} is the {@code JWE encrypted key}, which will be Base64URL-encoded + * and embedded in the resulting compact JWE string.

+ * + *

The result {@link #getKey() key} is the {@code JWE Content Encryption Key (CEK)} which will be used to encrypt + * the JWE.

+ * + * @since JJWT_RELEASE_VERSION + */ +public interface KeyResult extends Message, KeySupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java new file mode 100644 index 00000000..df582971 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeySupplier.java @@ -0,0 +1,34 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * Provides access to a cryptographic {@link Key} necessary for signing, wrapping, encryption or decryption algorithms. + * + * @param the type of key provided by this supplier. + * @since JJWT_RELEASE_VERSION + */ +public interface KeySupplier { + + /** + * Returns the key to use for signing, wrapping, encryption or decryption depending on the type of operation. + * + * @return the key to use for signing, wrapping, encryption or decryption depending on the type of operation. + */ + K getKey(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index af75e0e5..443e1f43 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -15,16 +15,13 @@ */ package io.jsonwebtoken.security; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.Jwts; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.KeyPair; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; /** * Utility class for securely generating {@link SecretKey}s and {@link KeyPair}s. @@ -33,36 +30,14 @@ import java.util.List; */ public final class Keys { - private static final String MAC = "io.jsonwebtoken.impl.crypto.MacProvider"; - private static final String RSA = "io.jsonwebtoken.impl.crypto.RsaProvider"; - private static final String EC = "io.jsonwebtoken.impl.crypto.EllipticCurveProvider"; - - private static final Class[] SIG_ARG_TYPES = new Class[]{SignatureAlgorithm.class}; - - //purposefully ordered higher to lower: - private static final List PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList( - SignatureAlgorithm.HS512, SignatureAlgorithm.HS384, SignatureAlgorithm.HS256)); + private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge"; + private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); + private static final Class[] FOR_PASSWORD_ARG_TYPES = new Class[]{char[].class}; //prevent instantiation private Keys() { } - /* - public static final int bitLength(Key key) throws IllegalArgumentException { - Assert.notNull(key, "Key cannot be null."); - if (key instanceof SecretKey) { - byte[] encoded = key.getEncoded(); - return Arrays.length(encoded) * 8; - } else if (key instanceof RSAKey) { - return ((RSAKey)key).getModulus().bitLength(); - } else if (key instanceof ECKey) { - return ((ECKey)key).getParams().getOrder().bitLength(); - } - - throw new IllegalArgumentException("Unsupported key type: " + key.getClass().getName()); - } - */ - /** * Creates a new SecretKey instance for use with HMAC-SHA algorithms based on the specified key byte array. * @@ -80,24 +55,45 @@ public final class Keys { int bitLength = bytes.length * 8; - for (SignatureAlgorithm alg : PREFERRED_HMAC_ALGS) { - if (bitLength >= alg.getMinKeyLength()) { - return new SecretKeySpec(bytes, alg.getJcaName()); - } + //Purposefully ordered higher to lower to ensure the strongest key possible can be generated. + if (bitLength >= 512) { + return new SecretKeySpec(bytes, "HmacSHA512"); + } else if (bitLength >= 384) { + return new SecretKeySpec(bytes, "HmacSHA384"); + } else if (bitLength >= 256) { + return new SecretKeySpec(bytes, "HmacSHA256"); } String msg = "The specified key byte array is " + bitLength + " bits which " + - "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + - "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the " + Keys.class.getName() + "#secretKeyFor(SignatureAlgorithm) method " + - "to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See " + - "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + + "size >= 256 bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the StandardSecureDigestAlgorithms.HS256.keyBuilder() method (or HS384.keyBuilder() " + + "or HS512.keyBuilder()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " + + "algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; throw new WeakKeyException(msg); } /** - * Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}. + *

Deprecation Notice

+ * + *

As of JJWT JJWT_RELEASE_VERSION, symmetric (secret) key algorithm instances can generate a key of suitable + * length for that specific algorithm by calling their {@code keyBuilder()} method directly. For example:

+ * + *

+     * {@link StandardSecureDigestAlgorithms#HS256}.keyBuilder().build();
+     * {@link StandardSecureDigestAlgorithms#HS384}.keyBuilder().build();
+     * {@link StandardSecureDigestAlgorithms#HS512}.keyBuilder().build();
+     * 
+ * + *

Call those methods as needed instead of this static {@code secretKeyFor} helper method - the returned + * {@link KeyBuilder} allows callers to specify a preferred Provider or SecureRandom on the builder if + * desired, whereas this {@code secretKeyFor} method does not. Consequently this helper method will be removed + * before the 1.0 release.

+ * + *

Previous Documentation

+ * + *

Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}.

* *

JWA Specification (RFC 7518), Section 3.2 * requires minimum key lengths to be used for each respective Signature Algorithm. This method returns a @@ -125,24 +121,44 @@ public final class Keys { * * @param alg the {@code SignatureAlgorithm} to inspect to determine which key length to use. * @return a new {@link SecretKey} instance suitable for use with the specified {@link SignatureAlgorithm}. - * @throws IllegalArgumentException for any input value other than {@link SignatureAlgorithm#HS256}, - * {@link SignatureAlgorithm#HS384}, or {@link SignatureAlgorithm#HS512} + * @throws IllegalArgumentException for any input value other than {@link io.jsonwebtoken.SignatureAlgorithm#HS256}, + * {@link io.jsonwebtoken.SignatureAlgorithm#HS384}, or {@link io.jsonwebtoken.SignatureAlgorithm#HS512} + * @deprecated since JJWT_RELEASE_VERSION. Use your preferred {@link MacAlgorithm} instance's + * {@link MacAlgorithm#keyBuilder() keyBuilder()} method directly. */ - public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgumentException { + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - switch (alg) { - case HS256: - case HS384: - case HS512: - return Classes.invokeStatic(MAC, "generateKey", SIG_ARG_TYPES, alg); - default: - String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; - throw new IllegalArgumentException(msg); + SecureDigestAlgorithm salg = Jwts.SIG.get(alg.name()); + if (!(salg instanceof MacAlgorithm)) { + String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; + throw new IllegalArgumentException(msg); } + return ((MacAlgorithm) salg).keyBuilder().build(); } /** - * Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. + *

Deprecation Notice

+ * + *

As of JJWT JJWT_RELEASE_VERSION, asymmetric key algorithm instances can generate KeyPairs of suitable strength + * for that specific algorithm by calling their {@code keyPairBuilder()} method directly. For example:

+ * + *
+     * Jwts.SIG.{@link StandardSecureDigestAlgorithms#RS256 RS256}.keyPairBuilder().build();
+     * Jwts.SIG.{@link StandardSecureDigestAlgorithms#RS384 RS384}.keyPairBuilder().build();
+     * Jwts.SIG.{@link StandardSecureDigestAlgorithms#RS512 RS512}.keyPairBuilder().build();
+     * ... etc ...
+     * Jwts.SIG.{@link StandardSecureDigestAlgorithms#ES512 ES512}.keyPairBuilder().build();
+ * + *

Call those methods as needed instead of this static {@code keyPairFor} helper method - the returned + * {@link KeyPairBuilder} allows callers to specify a preferred Provider or SecureRandom on the builder if + * desired, whereas this {@code keyPairFor} method does not. Consequently this helper method will be removed + * before the 1.0 release.

+ * + *

Previous Documentation

+ * + *

Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm.

* *

If the {@code alg} argument is an RSA algorithm, a KeyPair is generated based on the following:

* @@ -202,7 +218,7 @@ public final class Keys { * * * EC512 - * 512 bits + * 521 bits * {@code P-521} * {@code secp521r1} * @@ -211,24 +227,44 @@ public final class Keys { * @param alg the {@code SignatureAlgorithm} to inspect to determine which asymmetric algorithm to use. * @return a new {@link KeyPair} suitable for use with the specified asymmetric algorithm. * @throws IllegalArgumentException if {@code alg} is not an asymmetric algorithm + * @deprecated since JJWT_RELEASE_VERSION in favor of your preferred + * {@link io.jsonwebtoken.security.SignatureAlgorithm} instance's + * {@link SignatureAlgorithm#keyPairBuilder() keyPairBuilder()} method directly. */ - public static KeyPair keyPairFor(SignatureAlgorithm alg) throws IllegalArgumentException { + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - switch (alg) { - case RS256: - case PS256: - case RS384: - case PS384: - case RS512: - case PS512: - return Classes.invokeStatic(RSA, "generateKeyPair", SIG_ARG_TYPES, alg); - case ES256: - case ES384: - case ES512: - return Classes.invokeStatic(EC, "generateKeyPair", SIG_ARG_TYPES, alg); - default: - String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; - throw new IllegalArgumentException(msg); + SecureDigestAlgorithm salg = Jwts.SIG.get(alg.name()); + if (!(salg instanceof SignatureAlgorithm)) { + String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; + throw new IllegalArgumentException(msg); } + SignatureAlgorithm asalg = ((SignatureAlgorithm) salg); + return asalg.keyPairBuilder().build(); + } + + /** + * Returns a new {@link Password} instance suitable for use with password-based key derivation algorithms. + * + *

Usage Note: Using {@code Password}s outside of key derivation contexts will likely + * fail. See the {@link Password} JavaDoc for more, and also note the Password Safety section below.

+ * + *

Password Safety

+ * + *

Instances returned by this method use a clone of the specified {@code password} character array + * argument - changes to the argument array will NOT be reflected in the returned key, and vice versa. If you wish + * to clear a {@code Password} instance to ensure it is no longer usable, call its {@link Password#destroy()} + * method will clear/overwrite its internal cloned char array. Also note that each subsequent call to + * {@link Password#toCharArray()} will also return a new clone of the underlying password character array per + * standard JCE key behavior.

+ * + * @param password the raw password character array to clone for use with password-based key derivation algorithms. + * @return a new {@link Password} instance that wraps a new clone of the specified {@code password} character array. + * @see Password#toCharArray() + * @since JJWT_RELEASE_VERSION + */ + public static Password forPassword(char[] password) { + return Classes.invokeStatic(BRIDGE_CLASS, "forPassword", FOR_PASSWORD_ARG_TYPES, new Object[]{password}); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/MacAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/MacAlgorithm.java new file mode 100644 index 00000000..a1550dd5 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/MacAlgorithm.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2022 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.security; + +import io.jsonwebtoken.Identifiable; + +import javax.crypto.SecretKey; + +/** + * A {@link SecureDigestAlgorithm} that uses symmetric {@link SecretKey}s to both compute and verify digests as + * message authentication codes (MACs). + * + *

Standard Identifier

+ * + *

{@code MacAlgorithm} extends {@link Identifiable}: when a {@code MacAlgorithm} is used to compute the MAC of a + * JWS, the value returned from {@link Identifiable#getId() macAlgorithm.getId()} will be set as the JWS + * "alg" protected header value.

+ * + *

Key Strength

+ * + *

MAC algorithm strength is in part attributed to how difficult it is to discover the secret key. + * As such, MAC algorithms usually require keys of a minimum length to ensure the keys are difficult to discover + * and the algorithm's security properties are maintained.

+ * + *

The {@code MacAlgorithm} interface extends the {@link KeyLengthSupplier} interface to represent + * the length in bits (not bytes) a key must have to be used with its implementation. If you do not want to + * worry about lengths and parameters of keys required for an algorithm, it is often easier to automatically generate + * a key that adheres to the algorithms requirements, as discussed below.

+ * + *

Key Generation

+ * + *

{@code MacAlgorithm} extends {@link KeyBuilderSupplier} to enable {@link SecretKey} generation. + * Each {@code MacAlgorithm} algorithm instance will return a {@link KeyBuilder} that ensures any created keys will + * have a sufficient length and any algorithm parameters required by that algorithm. For example:

+ * + *
+ * SecretKey key = macAlgorithm.keyBuilder().build();
+ * + *

The resulting {@code key} is guaranteed to have the correct algorithm parameters and strength/length necessary for + * that exact {@code MacAlgorithm} instance.

+ * + *

JWA Standard Implementations

+ * + *

Constant definitions and utility methods for all JWA (RFC 7518) standard MAC algorithms are + * available via the {@link StandardSecureDigestAlgorithms} registry singleton.

+ * + * @see StandardSecureDigestAlgorithms + * @since JJWT_RELEASE_VERSION + */ +public interface MacAlgorithm extends SecureDigestAlgorithm, + KeyBuilderSupplier, KeyLengthSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java new file mode 100644 index 00000000..d0bb8de8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/MalformedKeyException.java @@ -0,0 +1,44 @@ +/* + * 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.security; + +/** + * Exception thrown when encountering a key or key material that is incomplete or improperly configured or + * formatted and cannot be used as expected. + * + * @since JJWT_RELEASE_VERSION + */ +public class MalformedKeyException extends InvalidKeyException { + + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ + public MalformedKeyException(String message) { + super(message); + } + + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param msg the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public MalformedKeyException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Message.java b/api/src/main/java/io/jsonwebtoken/security/Message.java new file mode 100644 index 00000000..136925c1 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Message.java @@ -0,0 +1,36 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * A message contains a {@link #getPayload() payload} used as input to or output from a cryptographic algorithm. + * + * @param The type of payload in the message. + * @since JJWT_RELEASE_VERSION + */ +public interface Message { + + /** + * Returns the message payload used as input to or output from a cryptographic algorithm. This is almost always + * plaintext used for cryptographic signatures or encryption, or ciphertext for decryption, or a {@link Key} + * instance for wrapping or unwrapping algorithms. + * + * @return the message payload used as input to or output from a cryptographic algorithm. + */ + T getPayload(); //plaintext, ciphertext or Key +} diff --git a/api/src/main/java/io/jsonwebtoken/security/OctetPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/OctetPrivateJwk.java new file mode 100644 index 00000000..efc5d7fe --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/OctetPrivateJwk.java @@ -0,0 +1,68 @@ +/* + * 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPrivateKey; + +/** + * JWK representation of an Edwards Curve + * {@link PrivateKey} as defined by RFC 8037, Section 2: + * Key Type "OKP". + * + *

Unlike the {@link EcPrivateJwk} interface, which only supports + * Weierstrass-form {@link ECPrivateKey}s, + * {@code OctetPrivateJwk} allows for multiple parameterized {@link PrivateKey} types + * because the JDK supports two different types of Edwards Curve private keys:

+ * + *

As such, {@code OctetPrivateJwk} is parameterized to support both key types.

+ * + *

Earlier JDK Versions

+ * + *

Even though {@code XECPrivateKey} and {@code EdECPrivateKey} were introduced in JDK 11 and JDK 15 respectively, + * JJWT supports Octet private JWKs in earlier versions when BouncyCastle is enabled in the application classpath. When + * using earlier JDK versions, the {@code OctetPrivateJwk} instance will need be parameterized with the + * generic {@code PrivateKey} type since the latter key types would not be present. For example:

+ *
+ * OctetPrivateJwk<PrivateKey> octetPrivateJwk = getKey();
+ * + *

OKP-specific Properties

+ * + *

Note that the various OKP-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link PrivateKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + *

Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

+ *
+ * jwk.get("x");
+ * jwk.get("d");
+ * // ... etc ...
+ * + * @param The type of Edwards-curve {@link PrivateKey} represented by this JWK (e.g. XECPrivateKey, EdECPrivateKey, etc). + * @param The type of Edwards-curve {@link PublicKey} represented by the JWK's corresponding + * {@link #toPublicJwk() public JWK}, for example XECPublicKey, EdECPublicKey, etc. + * @since JJWT_RELEASE_VERSION + */ +public interface OctetPrivateJwk extends PrivateJwk> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/OctetPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/OctetPrivateJwkBuilder.java new file mode 100644 index 00000000..def06532 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/OctetPrivateJwkBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * A {@link PrivateJwkBuilder} that creates {@link OctetPrivateJwk} instances. + * + * @param The type of {@link PrivateKey} represented by the constructed {@link OctetPrivateJwk} instance. + * @param The type of {@link PublicKey} available from the constructed {@link OctetPrivateJwk}'s associated {@link PrivateJwk#toPublicJwk() public JWK} properties. + * @since JJWT_RELEASE_VERSION + */ +public interface OctetPrivateJwkBuilder extends + PrivateJwkBuilder, OctetPrivateJwk, OctetPrivateJwkBuilder> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/OctetPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/OctetPublicJwk.java new file mode 100644 index 00000000..9c622cea --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/OctetPublicJwk.java @@ -0,0 +1,63 @@ +/* + * 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.security; + +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; + +/** + * JWK representation of an Edwards Curve + * {@link PublicKey} as defined by RFC 8037, Section 2: + * Key Type "OKP". + * + *

Unlike the {@link EcPublicJwk} interface, which only supports + * Weierstrass-form {@link ECPublicKey}s, + * {@code OctetPublicJwk} allows for multiple parameterized {@link PublicKey} types + * because the JDK supports two different types of Edwards Curve public keys:

+ * + *

As such, {@code OctetPublicJwk} is parameterized to support both key types.

+ * + *

Earlier JDK Versions

+ * + *

Even though {@code XECPublicKey} and {@code EdECPublicKey} were introduced in JDK 11 and JDK 15 respectively, + * JJWT supports Octet public JWKs in earlier versions when BouncyCastle is enabled in the application classpath. When + * using earlier JDK versions, the {@code OctetPublicJwk} instance will need be parameterized with the + * generic {@code PublicKey} type since the latter key types would not be present. For example:

+ *
OctetPublicJwk<PublicKey> octetPublicJwk = getKey();
+ * + *

OKP-specific Properties

+ * + *

Note that the various OKP-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link PublicKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + *

Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

+ *
+ * jwk.get("x");
+ * // ... etc ...
+ * + * @param The type of Edwards-curve {@link PublicKey} represented by this JWK (e.g. XECPublicKey, EdECPublicKey, etc). + * @since JJWT_RELEASE_VERSION + */ +public interface OctetPublicJwk extends PublicJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/OctetPublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/OctetPublicJwkBuilder.java new file mode 100644 index 00000000..abf5d27e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/OctetPublicJwkBuilder.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * A {@link PublicJwkBuilder} that creates {@link OctetPublicJwk} instances. + * + * @param the type of {@link PublicKey} provided by the created {@link OctetPublicJwk} (e.g. XECPublicKey, EdECPublicKey, etc). + * @param the type of {@link PrivateKey} that may be paired with the {@link PublicKey} to produce an + * {@link OctetPrivateJwk} if desired. For example, XECPrivateKey, EdECPrivateKey, etc. + * @since JJWT_RELEASE_VERSION + */ +public interface OctetPublicJwkBuilder + extends PublicJwkBuilder, OctetPrivateJwk, OctetPrivateJwkBuilder, OctetPublicJwkBuilder> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Password.java b/api/src/main/java/io/jsonwebtoken/security/Password.java new file mode 100644 index 00000000..50c0c380 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Password.java @@ -0,0 +1,63 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; +import javax.security.auth.Destroyable; + +/** + * A {@code Key} suitable for use with password-based key derivation algorithms. + * + *

Usage Warning

+ * + *

Because raw passwords should never be used as direct inputs for cryptographic operations (such as authenticated + * hashing or encryption) - and only for derivation algorithms (like password-based encryption) - {@code Password} + * instances will throw an exception when used in these invalid contexts. Specifically, calling a + * {@code Password}'s {@link Password#getEncoded() getEncoded()} method (as would be done automatically by the + * JCA subsystem during direct cryptographic operations) will throw an + * {@link UnsupportedOperationException UnsupportedOperationException}.

+ * + * @see #toCharArray() + * @since JJWT_RELEASE_VERSION + */ +public interface Password extends SecretKey, Destroyable { + + /** + * Returns a new clone of the underlying password character array for use during derivation algorithms. Like all + * {@code SecretKey} implementations, if you wish to clear the backing password character array for + * safety/security reasons, call the {@link #destroy()} method, ensuring that both the character array is cleared + * and the {@code Password} instance can no longer be used. + * + *

Usage

+ * + *

Because a new clone is returned from this method each time it is invoked, it is expected that callers will + * clear the resulting clone from memory as soon as possible to reduce probability of password exposure. For + * example:

+ * + *

+     * char[] clonedPassword = aPassword.toCharArray();
+     * try {
+     *     doSomethingWithPassword(clonedPassword);
+     * } finally {
+     *     // guarantee clone is cleared regardless of any Exception thrown:
+     *     java.util.Arrays.fill(clonedPassword, '\u0000');
+     * }
+     * 
+ * + * @return a clone of the underlying password character array. + */ + char[] toCharArray(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java new file mode 100644 index 00000000..1c65142c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwk.java @@ -0,0 +1,61 @@ +/* + * 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * JWK representation of a {@link PrivateKey}. + * + *

JWK Private Key vs Java {@code PrivateKey} differences

+ * + *

Unlike the Java cryptography APIs, the JWK specification requires all public key and private key + * properties to be contained within every private JWK. As such, a {@code PrivateJwk} of course represents + * private key fields as its name implies, but it is probably more similar to the Java JCA concept of a + * {@link java.security.KeyPair} since it contains everything for both keys.

+ * + *

Consequently a {@code PrivateJwk} is capable of providing two additional convenience methods:

+ *
    + *
  • {@link #toPublicJwk()} - a method to obtain a {@link PublicJwk} instance that contains only the JWK public + * key properties, and
  • + *
  • {@link #toKeyPair()} - a method to obtain both Java {@link PublicKey} and {@link PrivateKey}s in aggregate + * as a {@link KeyPair} instance if desired.
  • + *
+ * + * @param The type of {@link PrivateKey} represented by this JWK + * @param The type of {@link PublicKey} represented by the JWK's corresponding {@link #toPublicJwk() public JWK}. + * @param The type of {@link PublicJwk} reflected by the JWK's public properties. + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateJwk> extends AsymmetricJwk { + + /** + * Returns the private JWK's corresponding {@link PublicJwk}, containing only the key's public properties. + * + * @return the private JWK's corresponding {@link PublicJwk}, containing only the key's public properties. + */ + M toPublicJwk(); + + /** + * Returns the key's corresponding Java {@link PrivateKey} and {@link PublicKey} in aggregate as a + * type-safe {@link KeyPair} instance. + * + * @return the key's corresponding Java {@link PrivateKey} and {@link PublicKey} in aggregate as a + * type-safe {@link KeyPair} instance. + */ + KeyPair toKeyPair(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java new file mode 100644 index 00000000..f51e14b9 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateJwkBuilder.java @@ -0,0 +1,53 @@ +/* + * 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * An {@link AsymmetricJwkBuilder} that creates {@link PrivateJwk} instances. + * + * @param the type of Java {@link PrivateKey} provided by the created private JWK. + * @param the type of Java {@link PublicKey} paired with the private key. + * @param the type of {@link PrivateJwk} created + * @param the type of {@link PublicJwk} paired with the created private JWK. + * @param the type of the builder, for subtype method chaining + * @see #setPublicKey(PublicKey) + * @since JJWT_RELEASE_VERSION + */ +public interface PrivateJwkBuilder, M extends PrivateJwk, + T extends PrivateJwkBuilder> extends AsymmetricJwkBuilder { + + /** + * Allows specifying of the {@link PublicKey} associated with the builder's existing {@link PrivateKey}, + * offering a reasonable performance enhancement when building the final private JWK. Application developers + * should prefer to use this method when possible when building private JWKs. + * + *

As discussed in the {@link PrivateJwk} documentation, the JWK and JWA specifications require private JWKs to + * contain both private key and public key data. If a public key is not provided via this + * {@code setPublicKey} method, the builder implementation must go through the work to derive the + * {@code PublicKey} instance based on the {@code PrivateKey} to obtain the necessary public key information.

+ * + *

Calling this method with the {@code PrivateKey}'s matching {@code PublicKey} instance eliminates the need + * for the builder to do that work.

+ * + * @param publicKey the {@link PublicKey} that matches the builder's existing {@link PrivateKey}. + * @return the builder for method chaining. + */ + T setPublicKey(L publicKey); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java new file mode 100644 index 00000000..fd4b7d88 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/ProtoJwkBuilder.java @@ -0,0 +1,345 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.List; + +/** + * A prototypical {@link JwkBuilder} that coerces to a more type-specific builder based on the {@link Key} that will + * be represented as a JWK. + * + * @param the type of Java {@link Key} represented by the created {@link Jwk}. + * @param the type of {@link Jwk} created by the builder + * @since JJWT_RELEASE_VERSION + */ +public interface ProtoJwkBuilder> extends JwkBuilder> { + + /** + * Ensures the builder will create a {@link SecretJwk} for the specified Java {@link SecretKey}. + * + * @param key the {@link SecretKey} to represent as a {@link SecretJwk}. + * @return the builder coerced as a {@link SecretJwkBuilder}. + */ + SecretJwkBuilder forKey(SecretKey key); + + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link RSAPublicKey}. + * + * @param key the {@link RSAPublicKey} to represent as a {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ + RsaPublicJwkBuilder forKey(RSAPublicKey key); + + /** + * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java {@link RSAPrivateKey}. If + * possible, it is recommended to also call the resulting builder's + * {@link RsaPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method with the private key's matching + * {@link PublicKey} for better performance. See the + * {@link RsaPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} and {@link PrivateJwk} JavaDoc for more + * information. + * + * @param key the {@link RSAPublicKey} to represent as a {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPrivateJwkBuilder}. + */ + RsaPrivateJwkBuilder forKey(RSAPrivateKey key); + + /** + * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link ECPublicKey}. + * + * @param key the {@link ECPublicKey} to represent as a {@link EcPublicJwk}. + * @return the builder coerced as an {@link EcPublicJwkBuilder}. + */ + EcPublicJwkBuilder forKey(ECPublicKey key); + + /** + * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java {@link ECPrivateKey}. If + * possible, it is recommended to also call the resulting builder's + * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method with the private key's matching + * {@link PublicKey} for better performance. See the + * {@link EcPrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} and {@link PrivateJwk} JavaDoc for more + * information. + * + * @param key the {@link ECPublicKey} to represent as an {@link EcPublicJwk}. + * @return the builder coerced as a {@link EcPrivateJwkBuilder}. + */ + EcPrivateJwkBuilder forKey(ECPrivateKey key); + + + /** + * Ensures the builder will create a {@link PublicJwk} for the specified Java {@link PublicKey} argument. This + * method is provided for congruence with the other {@code forKey} methods and is expected to be used when + * the calling code has an untyped {@code PublicKey} reference. Based on the argument type, it will delegate to one + * of the following methods if possible: + *
    + *
  • {@link #forKey(RSAPublicKey)}
  • + *
  • {@link #forKey(ECPublicKey)}
  • + *
  • {@link #forOctetKey(PublicKey)}
  • + *
+ * + *

If the specified {@code key} argument is not capable of being supported by one of those methods, an + * {@link UnsupportedKeyException} will be thrown.

+ * + *

Type Parameters

+ * + *

In addition to the public key type A, the public key's associated private key type + * B is parameterized as well. This ensures that any subsequent call to the builder's + * {@link PublicJwkBuilder#setPrivateKey(PrivateKey) setPrivateKey} method will be type-safe. For example:

+ * + *
Jwks.builder().<EdECPublicKey, EdECPrivateKey>forKey(anEdECPublicKey)
+     *     .setPrivateKey(aPrivateKey) // <-- must be an EdECPrivateKey instance
+     *     ... etc ...
+     *     .build();
+ * + * @param
the type of {@link PublicKey} provided by the created public JWK. + * @param the type of {@link PrivateKey} that may be paired with the {@link PublicKey} to produce a + * {@link PrivateJwk} if desired. + * @param key the {@link PublicKey} to represent as a {@link PublicJwk}. + * @return the builder coerced as a {@link PublicJwkBuilder} for continued method chaining. + * @throws UnsupportedKeyException if the specified key is not a supported type and cannot be used to delegate to + * other {@code forKey} methods. + * @see PublicJwk + * @see PrivateJwk + */ + PublicJwkBuilder forKey(A key) throws UnsupportedKeyException; + + /** + * Ensures the builder will create a {@link PrivateJwk} for the specified Java {@link PrivateKey} argument. This + * method is provided for congruence with the other {@code forKey} methods and is expected to be used when + * the calling code has an untyped {@code PrivateKey} reference. Based on the argument type, it will delegate to one + * of the following methods if possible: + *
    + *
  • {@link #forKey(RSAPrivateKey)}
  • + *
  • {@link #forKey(ECPrivateKey)}
  • + *
  • {@link #forOctetKey(PrivateKey)}
  • + *
+ * + *

If the specified {@code key} argument is not capable of being supported by one of those methods, an + * {@link UnsupportedKeyException} will be thrown.

+ * + *

Type Parameters

+ * + *

In addition to the private key type B, the private key's associated public key type + * A is parameterized as well. This ensures that any subsequent call to the builder's + * {@link PrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method will be type-safe. For example:

+ * + *
Jwks.builder().<EdECPublicKey, EdECPrivateKey>forKey(anEdECPrivateKey)
+     *     .setPublicKey(aPublicKey) // <-- must be an EdECPublicKey instance
+     *     ... etc ...
+     *     .build();
+ * + * @param
the type of {@link PublicKey} paired with the {@code key} argument to produce the {@link PrivateJwk}. + * @param the type of the {@link PrivateKey} argument. + * @param key the {@link PrivateKey} to represent as a {@link PrivateJwk}. + * @return the builder coerced as a {@link PrivateJwkBuilder} for continued method chaining. + * @throws UnsupportedKeyException if the specified key is not a supported type and cannot be used to delegate to + * other {@code forKey} methods. + * @see PublicJwk + * @see PrivateJwk + */ + PrivateJwkBuilder forKey(B key) throws UnsupportedKeyException; + + /** + * Ensures the builder will create an {@link OctetPublicJwk} for the specified Edwards-curve {@code PublicKey} + * argument. The {@code PublicKey} must be an instance of one of the following: + * + * + *

Type Parameters

+ * + *

In addition to the public key type A, the public key's associated private key type + * B is parameterized as well. This ensures that any subsequent call to the builder's + * {@link PublicJwkBuilder#setPrivateKey(PrivateKey) setPrivateKey} method will be type-safe. For example:

+ * + *
Jwks.builder().<EdECPublicKey, EdECPrivateKey>forKey(anEdECPublicKey)
+     *     .setPrivateKey(aPrivateKey) // <-- must be an EdECPrivateKey instance
+     *     ... etc ...
+     *     .build();
+ * + * @param the type of Edwards-curve {@link PublicKey} provided by the created public JWK. + * @param the type of Edwards-curve {@link PrivateKey} that may be paired with the {@link PublicKey} to produce + * an {@link OctetPrivateJwk} if desired. + * @param key the Edwards-curve {@link PublicKey} to represent as an {@link OctetPublicJwk}. + * @return the builder coerced as a {@link OctetPublicJwkBuilder} for continued method chaining. + * @throws UnsupportedKeyException if the specified key is not a supported Edwards-curve key. + * @see java.security.interfaces.XECPublicKey + * @see java.security.interfaces.EdECPublicKey + */ + OctetPublicJwkBuilder forOctetKey(A key); + + /** + * Ensures the builder will create an {@link OctetPrivateJwk} for the specified Edwards-curve {@code PrivateKey} + * argument. The {@code PrivateKey} must be an instance of one of the following: + * + * + *

Type Parameters

+ * + *

In addition to the private key type B, the private key's associated public key type + * A is parameterized as well. This ensures that any subsequent call to the builder's + * {@link PrivateJwkBuilder#setPublicKey(PublicKey) setPublicKey} method will be type-safe. For example:

+ * + *
Jwks.builder().<EdECPublicKey, EdECPrivateKey>forKey(anEdECPrivateKey)
+     *     .setPublicKey(aPublicKey) // <-- must be an EdECPublicKey instance
+     *     ... etc ...
+     *     .build();
+ * + * @param the type of the Edwards-curve {@link PrivateKey} argument. + * @param the type of Edwards-curve {@link PublicKey} paired with the {@code key} argument to produce the + * {@link OctetPrivateJwk}. + * @param key the Edwards-curve {@link PrivateKey} to represent as an {@link OctetPrivateJwk}. + * @return the builder coerced as an {@link OctetPrivateJwkBuilder} for continued method chaining. + * @throws UnsupportedKeyException if the specified key is not a supported Edwards-curve key. + * @see java.security.interfaces.XECPrivateKey + * @see java.security.interfaces.EdECPrivateKey + */ + OctetPrivateJwkBuilder forOctetKey(A key); + + /** + * Ensures the builder will create an {@link OctetPrivateJwk} for the specified Java Edwards-curve + * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an + * Edwards-curve public key as defined by {@link #forOctetKey(PublicKey)}. The pair's + * {@link KeyPair#getPrivate() private key} MUST be an Edwards-curve private key as defined by + * {@link #forOctetKey(PrivateKey)}. + * + * @param the type of Edwards-curve {@link PublicKey} contained in the key pair. + * @param the type of the Edwards-curve {@link PrivateKey} contained in the key pair. + * @param keyPair the Edwards-curve {@link KeyPair} to represent as an {@link OctetPrivateJwk}. + * @return the builder coerced as an {@link OctetPrivateJwkBuilder} for continued method chaining. + * @throws IllegalArgumentException if the {@code keyPair} does not contain Edwards-curve public and private key + * instances. + */ + OctetPrivateJwkBuilder forOctetKeyPair(KeyPair keyPair); + + /** + * Ensures the builder will create an {@link OctetPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at list index 0) MUST + * {@link X509Certificate#getPublicKey() contain} an Edwards-curve public key as defined by + * {@link #forOctetKey(PublicKey)}. + * + * @param the type of Edwards-curve {@link PublicKey} contained in the first {@code X509Certificate}. + * @param the type of Edwards-curve {@link PrivateKey} that may be paired with the {@link PublicKey} to produce + * an {@link OctetPrivateJwk} if desired. + * @param chain the {@link X509Certificate} chain to inspect to find the Edwards-curve {@code PublicKey} to + * represent as an {@link OctetPublicJwk}. + * @return the builder coerced as an {@link OctetPublicJwkBuilder} for continued method chaining. + */ + OctetPublicJwkBuilder forOctetChain(List chain); + + /** + * Ensures the builder will create an {@link OctetPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at array index 0) MUST + * {@link X509Certificate#getPublicKey() contain} an Edwards-curve public key as defined by + * {@link #forOctetKey(PublicKey)}. + * + * @param the type of Edwards-curve {@link PublicKey} contained in the first {@code X509Certificate}. + * @param the type of Edwards-curve {@link PrivateKey} that may be paired with the {@link PublicKey} to produce + * an {@link OctetPrivateJwk} if desired. + * @param chain the {@link X509Certificate} chain to inspect to find the Edwards-curve {@code PublicKey} to + * represent as an {@link OctetPublicJwk}. + * @return the builder coerced as an {@link OctetPublicJwkBuilder} for continued method chaining. + */ + OctetPublicJwkBuilder forOctetChain(X509Certificate... chain); + + /** + * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link ECPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link ECPublicKey} to represent as a + * {@link EcPublicJwk}. + * @return the builder coerced as an {@link EcPublicJwkBuilder}. + */ + EcPublicJwkBuilder forEcChain(X509Certificate... chain); + + /** + * Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at list index 0) MUST contain an {@link ECPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link ECPublicKey} to represent as a + * {@link EcPublicJwk}. + * @return the builder coerced as an {@link EcPublicJwkBuilder}. + */ + EcPublicJwkBuilder forEcChain(List chain); + + /** + * Ensures the builder will create an {@link EcPrivateJwk} for the specified Java Elliptic Curve + * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an + * {@link ECPublicKey} instance. The pair's {@link KeyPair#getPrivate() private key} MUST be an + * {@link ECPrivateKey} instance. + * + * @param keyPair the EC {@link KeyPair} to represent as an {@link EcPrivateJwk}. + * @return the builder coerced as an {@link EcPrivateJwkBuilder}. + * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link ECPublicKey} and + * {@link ECPrivateKey} instances. + */ + EcPrivateJwkBuilder forEcKeyPair(KeyPair keyPair) throws IllegalArgumentException; + + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at array index 0) MUST contain an {@link RSAPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a + * {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ + RsaPublicJwkBuilder forRsaChain(X509Certificate... chain); + + /** + * Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain. + * The first {@code X509Certificate} in the chain (at list index 0) MUST contain an {@link RSAPublicKey} + * instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method. + * + * @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a + * {@link RsaPublicJwk}. + * @return the builder coerced as an {@link RsaPublicJwkBuilder}. + */ + RsaPublicJwkBuilder forRsaChain(List chain); + + /** + * Ensures the builder will create an {@link RsaPrivateJwk} for the specified Java RSA + * {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} MUST be an + * {@link RSAPublicKey} instance. The pair's {@link KeyPair#getPrivate() private key} MUST be an + * {@link RSAPrivateKey} instance. + * + * @param keyPair the RSA {@link KeyPair} to represent as an {@link RsaPrivateJwk}. + * @return the builder coerced as an {@link RsaPrivateJwkBuilder}. + * @throws IllegalArgumentException if the {@code keyPair} does not contain {@link RSAPublicKey} and + * {@link RSAPrivateKey} instances. + */ + RsaPrivateJwkBuilder forRsaKeyPair(KeyPair keyPair) throws IllegalArgumentException; +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java new file mode 100644 index 00000000..f7aa6d3e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwk.java @@ -0,0 +1,27 @@ +/* + * 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.security; + +import java.security.PublicKey; + +/** + * JWK representation of a {@link PublicKey}. + * + * @param The type of {@link PublicKey} represented by this JWK + * @since JJWT_RELEASE_VERSION + */ +public interface PublicJwk extends AsymmetricJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java new file mode 100644 index 00000000..adfcc5a3 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PublicJwkBuilder.java @@ -0,0 +1,47 @@ +/* + * 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.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * An {@link AsymmetricJwkBuilder} that creates {@link PublicJwk} instances. + * + * @param the type of {@link PublicKey} provided by the created public JWK. + * @param the type of {@link PrivateKey} that may be paired with the {@link PublicKey} to produce a {@link PrivateJwk} if desired. + * @param the type of {@link PublicJwk} created + * @param the type of {@link PrivateJwk} that matches the created {@link PublicJwk} + * @param

the type of {@link PrivateJwkBuilder} that matches this builder if a {@link PrivateJwk} is desired. + * @param the type of the builder, for subtype method chaining + * @see #setPrivateKey(PrivateKey) + * @since JJWT_RELEASE_VERSION + */ +public interface PublicJwkBuilder, M extends PrivateJwk, + P extends PrivateJwkBuilder, + T extends PublicJwkBuilder> extends AsymmetricJwkBuilder { + + /** + * Sets the {@link PrivateKey} that pairs with the builder's existing {@link PublicKey}, converting this builder + * into a {@link PrivateJwkBuilder} which will produce a corresponding {@link PrivateJwk} instance. The + * specified {@code privateKey} MUST be the exact private key paired with the builder's public key. + * + * @param privateKey the {@link PrivateKey} that pairs with the builder's existing {@link PublicKey} + * @return the builder coerced as a {@link PrivateJwkBuilder} which will produce a corresponding {@link PrivateJwk}. + */ + P setPrivateKey(L privateKey); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/Request.java b/api/src/main/java/io/jsonwebtoken/security/Request.java new file mode 100644 index 00000000..a879c53b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/Request.java @@ -0,0 +1,57 @@ +/* + * 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.security; + +import java.security.Provider; +import java.security.SecureRandom; + +/** + * A {@code Request} aggregates various parameters that may be used by a particular cryptographic algorithm. It and + * any of its subtypes implemented as a single object submitted to an algorithm effectively reflect the + * Parameter Object design pattern. This + * provides for a much cleaner request/result algorithm API instead of polluting the API with an excessive number of + * overloaded methods that would exist otherwise. + * + *

The {@code Request} interface specifically allows for JCA {@link Provider} and {@link SecureRandom} instances + * to be used during request execution, which allows more flexibility than forcing a single {@code Provider} or + * {@code SecureRandom} for all executions. {@code Request} subtypes provide additional parameters as necessary + * depending on the type of cryptographic algorithm invoked.

+ * + * @param the type of payload in the request. + * @see #getProvider() + * @see #getSecureRandom() + * @since JJWT_RELEASE_VERSION + */ +public interface Request extends Message { + + /** + * Returns the JCA provider that should be used for cryptographic operations during the request or + * {@code null} if the JCA subsystem preferred provider should be used. + * + * @return the JCA provider that should be used for cryptographic operations during the request or + * {@code null} if the JCA subsystem preferred provider should be used. + */ + Provider getProvider(); + + /** + * Returns the {@code SecureRandom} to use when performing cryptographic operations during the request, or + * {@code null} if a default {@link SecureRandom} should be used. + * + * @return the {@code SecureRandom} to use when performing cryptographic operations during the request, or + * {@code null} if a default {@link SecureRandom} should be used. + */ + SecureRandom getSecureRandom(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java new file mode 100644 index 00000000..9269a983 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwk.java @@ -0,0 +1,43 @@ +/* + * 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.security; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * JWK representation of an {@link RSAPrivateKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for RSA Keys and + * Parameters for RSA Private Keys. + * + *

Note that the various RSA-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link RSAPrivateKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + *

Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

+ *
+ * jwk.get("n");
+ * jwk.get("e");
+ * // ... etc ...
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPrivateJwk extends PrivateJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java new file mode 100644 index 00000000..c88b16ca --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPrivateJwkBuilder.java @@ -0,0 +1,27 @@ +/* + * 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.security; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * A {@link PrivateJwkBuilder} that creates {@link RsaPrivateJwk}s. + * + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPrivateJwkBuilder extends PrivateJwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java new file mode 100644 index 00000000..aa74a195 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwk.java @@ -0,0 +1,42 @@ +/* + * 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.security; + +import java.security.interfaces.RSAPublicKey; + +/** + * JWK representation of an {@link RSAPublicKey} as defined by the JWA (RFC 7518) specification sections on + * Parameters for RSA Keys and + * Parameters for RSA Public Keys. + * + *

Note that the various RSA-specific properties are not available as separate dedicated getter methods, as most Java + * applications should rarely, if ever, need to access these individual key properties since they typically represent + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link RSAPublicKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + *

Even so, because these properties exist and are readable by nature of every JWK being a + * {@link java.util.Map Map}, they are still accessible via the standard {@code Map} {@link #get(Object) get} method + * using an appropriate JWK parameter id, for example:

+ *
+ * jwk.get("n");
+ * jwk.get("e");
+ * // ... etc ...
+ * + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPublicJwk extends PublicJwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java new file mode 100644 index 00000000..d0fbfddc --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaPublicJwkBuilder.java @@ -0,0 +1,28 @@ +/* + * 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.security; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * A {@link PublicJwkBuilder} that creates {@link RsaPublicJwk}s. + * + * @since JJWT_RELEASE_VERSION + */ +public interface RsaPublicJwkBuilder extends PublicJwkBuilder { + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java new file mode 100644 index 00000000..675e3b1c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwk.java @@ -0,0 +1,33 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * JWK representation of a {@link SecretKey} as defined by the JWA (RFC 7518) specification section on + * Parameters for Symmetric Keys. + * + *

Note that the {@code SecretKey}-specific properties are not available as separate dedicated getter methods, as + * most Java applications should rarely, if ever, need to access these individual key properties since they typically + * internal key material and/or serialization details. If you need to access these key properties, it is usually + * recommended to obtain the corresponding {@link SecretKey} instance returned by {@link #toKey()} and + * query that instead.

+ * + * @since JJWT_RELEASE_VERSION + */ +public interface SecretJwk extends Jwk { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java new file mode 100644 index 00000000..e9ada6d9 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretJwkBuilder.java @@ -0,0 +1,26 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * A {@link JwkBuilder} that creates {@link SecretJwk}s. + * + * @since JJWT_RELEASE_VERSION + */ +public interface SecretJwkBuilder extends JwkBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java new file mode 100644 index 00000000..76ea6656 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyAlgorithm.java @@ -0,0 +1,26 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * A {@link KeyAlgorithm} that uses symmetric {@link SecretKey}s to obtain AEAD encryption and decryption keys. + * + * @since JJWT_RELEASE_VERSION + */ +public interface SecretKeyAlgorithm extends KeyAlgorithm, KeyBuilderSupplier, KeyLengthSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java new file mode 100644 index 00000000..6665eb54 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecretKeyBuilder.java @@ -0,0 +1,27 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; + +/** + * A {@link KeyBuilder} that creates new secure-random {@link SecretKey}s with a length sufficient to be used by + * the security algorithm that produced this builder. + * + * @since JJWT_RELEASE_VERSION + */ +public interface SecretKeyBuilder extends KeyBuilder { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecureDigestAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SecureDigestAlgorithm.java new file mode 100644 index 00000000..7fa2097d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecureDigestAlgorithm.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2022 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.security; + +import io.jsonwebtoken.Identifiable; + +import java.security.Key; + +/** + * A {@link DigestAlgorithm} that requires a {@link Key} to compute and verify the authenticity of digests using either + * digital signature or + * message + * authentication code algorithms. + * + *

Standard Identifier

+ * + *

{@code SecureDigestAlgorithm} extends {@link Identifiable}: when a {@code SecureDigestAlgorithm} is used to + * compute the digital signature or MAC of a JWS, the value returned from + * {@link Identifiable#getId() secureDigestAlgorithm.getId()} will be set as the JWS + * "alg" protected header value.

+ * + *

Standard Implementations

+ * + *

Constant definitions and utility methods for all JWA (RFC 7518) standard + * Cryptographic Algorithms for Digital Signatures and + * MACs are available via the {@link StandardSecureDigestAlgorithms} utility class.

+ * + *

"alg" identifier

+ * + *

{@code SecureDigestAlgorithm} extends {@link Identifiable}: the value returned from + * {@link Identifiable#getId() getId()} will be used as the JWS "alg" protected header value.

+ * + * @param the type of {@link Key} used to create digital signatures or message authentication codes + * @param the type of {@link Key} used to verify digital signatures or message authentication codes + * @see MacAlgorithm + * @see SignatureAlgorithm + * @since JJWT_RELEASE_VERSION + */ +public interface SecureDigestAlgorithm + extends DigestAlgorithm, VerifySecureDigestRequest> { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java new file mode 100644 index 00000000..cd0cd764 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecureRequest.java @@ -0,0 +1,27 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * A request to a cryptographic algorithm requiring a {@link Key}. + * + * @param they type of key used by the algorithm during the request + * @since JJWT_RELEASE_VERSION + */ +public interface SecureRequest extends Request, KeySupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java new file mode 100644 index 00000000..80e22c55 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityBuilder.java @@ -0,0 +1,51 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Builder; + +import java.security.Provider; +import java.security.SecureRandom; + +/** + * A Security-specific {@link Builder} that allows configuration of common JCA API parameters that might be used + * during instance creation, such as a {@link java.security.Provider} or {@link java.security.SecureRandom}. + * + * @param The type of object that will be created each time {@link #build()} is invoked. + * @see #setProvider(Provider) + * @see #setRandom(SecureRandom) + * @since JJWT_RELEASE_VERSION + */ +public interface SecurityBuilder> extends Builder { + + /** + * Sets the JCA Security {@link Provider} to use if necessary when calling {@link #build()}. This is an optional + * property - if not specified, the default JCA Provider will be used. + * + * @param provider the JCA Security Provider instance to use if necessary when building the new instance. + * @return the builder for method chaining. + */ + B setProvider(Provider provider); + + /** + * Sets the {@link SecureRandom} to use if necessary when calling {@link #build()}. This is an optional property + * - if not specified and one is required, a default {@code SecureRandom} will be used. + * + * @param random the {@link SecureRandom} instance to use if necessary when building the new instance. + * @return the builder for method chaining. + */ + B setRandom(SecureRandom random); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityException.java b/api/src/main/java/io/jsonwebtoken/security/SecurityException.java index 8b5f8abd..107600df 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SecurityException.java +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityException.java @@ -18,14 +18,28 @@ package io.jsonwebtoken.security; import io.jsonwebtoken.JwtException; /** + * A {@code JwtException} attributed to a problem with security-related elements, such as + * cryptographic keys, algorithms, or the underlying Java JCA API. + * * @since 0.10.0 */ public class SecurityException extends JwtException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public SecurityException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SecurityException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java new file mode 100644 index 00000000..cb6552fc --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureAlgorithm.java @@ -0,0 +1,55 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * A digital signature algorithm computes and + * verifies digests using asymmetric public/private key cryptography. + * + *

Standard Identifier

+ * + *

{@code SignatureAlgorithm} extends {@link Identifiable}: when a {@code SignatureAlgorithm} is used to compute + * a JWS digital signature, the value returned from {@link Identifiable#getId() signatureAlgorithm.getId()} will be + * set as the JWS "alg" protected header value.

+ * + *

Key Pair Generation

+ * + *

{@code SignatureAlgorithm} extends {@link KeyPairBuilderSupplier} to enable + * {@link KeyPair} generation. Each {@code SignatureAlgorithm} instance will return a + * {@link KeyPairBuilder} that ensures any created key pairs will have a sufficient length and algorithm parameters + * required by that algorithm. For example:

+ * + *
+ * KeyPair pair = signatureAlgorithm.keyPairBuilder().build();
+ * + *

The resulting {@code pair} is guaranteed to have the correct algorithm parameters and length/strength necessary + * for that exact {@code signatureAlgorithm} instance.

+ * + *

JWA Standard Implementations

+ * + *

Constant definitions and utility methods for all JWA (RFC 7518) standard signature algorithms are + * available via the {@link StandardSecureDigestAlgorithms} registry singleton.

+ * + * @see StandardSecureDigestAlgorithms + * @since JJWT_RELEASE_VERSION + */ +public interface SignatureAlgorithm extends SecureDigestAlgorithm, KeyPairBuilderSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java index 7cddb2ca..ad8a167f 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java @@ -16,14 +16,28 @@ package io.jsonwebtoken.security; /** + * Exception thrown if there is problem calculating or verifying a digital signature or message authentication code. + * * @since 0.10.0 */ +@SuppressWarnings("deprecation") public class SignatureException extends io.jsonwebtoken.SignatureException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public SignatureException(String message) { super(message); } + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param message the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ public SignatureException(String message, Throwable cause) { super(message, cause); } diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java new file mode 100644 index 00000000..3fd0a880 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureRequest.java @@ -0,0 +1,32 @@ +/* + * 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.security; + +import java.security.Key; + +/** + * A request to a {@link SignatureAlgorithm} to compute a digital signature or + * digital signature or + * message + * authentication code. + *

The content for signature input will be available via {@link #getPayload()}, and the key used to compute + * the signature will be available via {@link #getKey()}.

+ * + * @param the type of {@link Key} used to compute a digital signature or message authentication code + * @since JJWT_RELEASE_VERSION + */ +public interface SignatureRequest extends SecureRequest { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/StandardEncryptionAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/StandardEncryptionAlgorithms.java new file mode 100644 index 00000000..bb92ca66 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/StandardEncryptionAlgorithms.java @@ -0,0 +1,164 @@ +/* + * 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.security; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Registry; + +import java.util.Collection; + +/** + * {@link Registry} singleton containing all standard JWE + * Encryption Algorithms + * codified in the + * JSON Web Signature and Encryption Algorithms Registry. These are most commonly accessed via the + * {@link io.jsonwebtoken.Jwts#ENC} convenience alias when creating a JWE. For example: + *
+ * {@link Jwts#builder()}.
+ *     // ... etc ...
+ *     .encryptWith(secretKey, {@link Jwts#ENC}.A256GCM) // or A128GCM, A192GCM, etc...
+ *     .build()
+ *

Direct type-safe references as shown above are often better than calling {@link #get(String)} or + * {@link #find(String)} which can be susceptible to misspelled or otherwise invalid string values.

+ * + * @see #get() + * @see #get(String) + * @see #find(String) + * @see #values() + * @see AeadAlgorithm + * + * @since JJWT_RELEASE_VERSION + */ +public final class StandardEncryptionAlgorithms implements Registry { + + private static final Registry DELEGATE = + Classes.newInstance("io.jsonwebtoken.impl.security.StandardEncryptionAlgorithmsBridge"); + + private static final StandardEncryptionAlgorithms INSTANCE = new StandardEncryptionAlgorithms(); + + /** + * Returns this registry (a static singleton). + * + * @return this registry (a static singleton). + */ + public static StandardEncryptionAlgorithms get() { // named `get` to mimic java.util.function.Supplier + return INSTANCE; + } + + /** + * {@code AES_128_CBC_HMAC_SHA_256} authenticated encryption algorithm as defined by + * RFC 7518, Section 5.2.3. This algorithm + * requires a 256-bit (32 byte) key. + */ + public final AeadAlgorithm A128CBC_HS256 = get("A128CBC-HS256"); + + /** + * {@code AES_192_CBC_HMAC_SHA_384} authenticated encryption algorithm, as defined by + * RFC 7518, Section 5.2.4. This algorithm + * requires a 384-bit (48 byte) key. + */ + public final AeadAlgorithm A192CBC_HS384 = get("A192CBC-HS384"); + + /** + * {@code AES_256_CBC_HMAC_SHA_512} authenticated encryption algorithm, as defined by + * RFC 7518, Section 5.2.5. This algorithm + * requires a 512-bit (64 byte) key. + */ + public final AeadAlgorithm A256CBC_HS512 = get("A256CBC-HS512"); + + /** + * "AES GCM using 128-bit key" as defined by + * RFC 7518, Section 5.31. This + * algorithm requires a 128-bit (16 byte) key. + * + *

1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 7 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

+ */ + public final AeadAlgorithm A128GCM = get("A128GCM"); + + /** + * "AES GCM using 192-bit key" as defined by + * RFC 7518, Section 5.31. This + * algorithm requires a 192-bit (24 byte) key. + * + *

1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 7 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

+ */ + public final AeadAlgorithm A192GCM = get("A192GCM"); + + /** + * "AES GCM using 256-bit key" as defined by + * RFC 7518, Section 5.31. This + * algorithm requires a 256-bit (32 byte) key. + * + *

1 Requires Java 8 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 7 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

+ */ + public final AeadAlgorithm A256GCM = get("A256GCM"); + + /** + * Prevent external instantiation. + */ + private StandardEncryptionAlgorithms() { + } + + /** + * Returns all JWE-standard AEAD encryption algorithms as an unmodifiable collection. + * + * @return all JWE-standard AEAD encryption algorithms as an unmodifiable collection. + */ + public Collection values() { + return DELEGATE.values(); + } + + /** + * Returns the JWE-standard Encryption Algorithm with the specified + * {@code enc} algorithm identifier or + * throws an {@link IllegalArgumentException} if there is no JWE-standard algorithm for the specified + * {@code id}. If a JWE-standard instance result is not mandatory, consider using the {@link #find(String)} + * method instead. + * + * @param id a JWE standard {@code enc} algorithm identifier + * @return the associated Encryption Algorithm instance. + * @throws IllegalArgumentException if there is no JWE-standard algorithm for the specified identifier. + * @see #find(String) + * @see RFC 7518, Section 5.1 + */ + @Override + public AeadAlgorithm get(String id) { + return DELEGATE.get(id); + } + + /** + * Returns the JWE Encryption Algorithm with the specified + * {@code enc} algorithm identifier or + * {@code null} if a JWE-standard algorithm for the specified {@code id} cannot be found. If a JWE-standard + * instance must be resolved, consider using the {@link #get(String)} method instead. + * + * @param id a JWE standard {@code enc} algorithm identifier + * @return the associated standard Encryption Algorithm instance or {@code null} otherwise. + * @see #get(String) + * @see RFC 7518, Section 5.1 + */ + @Override + public AeadAlgorithm find(String id) { + return DELEGATE.find(id); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/StandardHashAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/StandardHashAlgorithms.java new file mode 100644 index 00000000..5b66e900 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/StandardHashAlgorithms.java @@ -0,0 +1,178 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Registry; + +import java.util.Collection; + +/** + * Registry of various (but not all) + * IANA Hash + * Algorithms commonly used to compute {@link JwkThumbprint JWK Thumbprint}s and ensure valid + * JWK Thumbprint URIs. For + * example: + *
+ * Jwks.{@link  JwkBuilder builder}()
+ *     // ... etc ...
+ *     .{@link JwkBuilder#setIdFromThumbprint(HashAlgorithm) setIdFromThumbprint}(Jwks.HASH.{@link StandardHashAlgorithms#SHA256 SHA256}) // <---
+ *     .build()
+ *

or

+ *
+ * HashAlgorithm hashAlg = Jwks.HASH.{@link StandardHashAlgorithms#SHA256 SHA256};
+ * {@link JwkThumbprint} thumbprint = aJwk.{@link Jwk#thumbprint(HashAlgorithm) thumbprint}(hashAlg);
+ * String rfcMandatoryPrefix = "urn:ietf:params:oauth:jwk-thumbprint:" + hashAlg.getId();
+ * assert thumbprint.toURI().toString().startsWith(rfcMandatoryPrefix);
+ * 
+ * + * @see #get() + * @see #values() + * @see #find(String) + * @see #get(String) + * @see HashAlgorithm + * @since JJWT_RELEASE_VERSION + */ +public final class StandardHashAlgorithms implements Registry { + + private static final Registry DELEGATE = + Classes.newInstance("io.jsonwebtoken.impl.security.StandardHashAlgorithmsBridge"); + + private static final StandardHashAlgorithms INSTANCE = new StandardHashAlgorithms(); + + /** + * Returns this registry (a static singleton). + * + * @return this registry (a static singleton). + */ + public static StandardHashAlgorithms get() { // named `get` to mimic java.util.function.Supplier + return INSTANCE; + } + + /** + * IANA + * hash algorithm with an {@link Identifiable#getId() id} (aka IANA "{@code Hash Name String}") + * value of {@code sha-256}. It is a {@code HashAlgorithm} alias for the native + * Java JCA {@code SHA-256} {@code MessageDigest} algorithm. + */ + public final HashAlgorithm SHA256 = get("sha-256"); + + /** + * IANA + * hash algorithm with an {@link Identifiable#getId() id} (aka IANA "{@code Hash Name String}") + * value of {@code sha-384}. It is a {@code HashAlgorithm} alias for the native + * Java JCA {@code SHA-384} {@code MessageDigest} algorithm. + */ + public final HashAlgorithm SHA384 = get("sha-384"); + + /** + * IANA + * hash algorithm with an {@link Identifiable#getId() id} (aka IANA "{@code Hash Name String}") + * value of {@code sha-512}. It is a {@code HashAlgorithm} alias for the native + * Java JCA {@code SHA-512} {@code MessageDigest} algorithm. + */ + public final HashAlgorithm SHA512 = get("sha-512"); + + /** + * IANA + * hash algorithm with an {@link Identifiable#getId() id} (aka IANA "{@code Hash Name String}") + * value of {@code sha3-256}. It is a {@code HashAlgorithm} alias for the native + * Java JCA {@code SHA3-256} {@code MessageDigest} algorithm. + *

This algorithm requires at least JDK 9 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

+ */ + public final HashAlgorithm SHA3_256 = get("sha3-256"); + + /** + * IANA + * hash algorithm with an {@link Identifiable#getId() id} (aka IANA "{@code Hash Name String}") + * value of {@code sha3-384}. It is a {@code HashAlgorithm} alias for the native + * Java JCA {@code SHA3-384} {@code MessageDigest} algorithm. + *

This algorithm requires at least JDK 9 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

+ */ + public final HashAlgorithm SHA3_384 = get("sha3-384"); + + /** + * IANA + * hash algorithm with an {@link Identifiable#getId() id} (aka IANA "{@code Hash Name String}") + * value of {@code sha3-512}. It is a {@code HashAlgorithm} alias for the native + * Java JCA {@code SHA3-512} {@code MessageDigest} algorithm. + *

This algorithm requires at least JDK 9 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

+ */ + public final HashAlgorithm SHA3_512 = get("sha3-512"); + + /** + * Prevent external instantiation. + */ + private StandardHashAlgorithms() { + } + + /** + * Returns common + * IANA Hash + * Algorithms as an unmodifiable collection. + * + * @return common + * IANA Hash + * Algorithms as an unmodifiable collection. + */ + public Collection values() { + return DELEGATE.values(); + } + + /** + * Returns the {@code HashAlgorithm} instance with the specified IANA algorithm {@code id}, or throws an + * {@link IllegalArgumentException} if there is no supported algorithm for the specified {@code id}. The + * {@code id} parameter is expected to equal one of the string values in the {@code Hash Name String} + * column within the + * IANA Named Information + * Hash Algorithm Registry table. If a supported instance result is not mandatory, consider using the + * {@link #find(String)} method instead. + * + * @param id an IANA {@code Hash Name String} hash algorithm identifier. + * @return the associated {@code HashAlgorithm} instance. + * @throws IllegalArgumentException if there is no supported algorithm for the specified identifier. + * @see #find(String) + * @see IANA Named + * Information Hash Algorithm Registry + */ + @Override + public HashAlgorithm get(String id) { + return DELEGATE.get(id); + } + + /** + * Returns the {@code HashAlgorithm} instance with the specified IANA algorithm {@code id}, or {@code null} if + * the specified {@code id} cannot be found. The {@code id} parameter is expected to equal one of the string + * values in the {@code Hash Name String} column within the + * IANA Named Information + * Hash Algorithm Registry table. If a standard instance must be resolved, consider using the + * {@link #get(String)} method instead. + * + * @param id an IANA {@code Hash Name String} hash algorithm identifier + * @return the associated {@code HashAlgorithm} instance if found or {@code null} otherwise. + * @see IANA Named Information + * Hash Algorithm Registry + * @see #get(String) + */ + @Override + public HashAlgorithm find(String id) { + return DELEGATE.find(id); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/StandardKeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/StandardKeyAlgorithms.java new file mode 100644 index 00000000..402c6b58 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/StandardKeyAlgorithms.java @@ -0,0 +1,712 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Registry; + +import javax.crypto.SecretKey; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collection; + +/** + * {@link Registry} singleton containing all standard JWE + * Key Management Algorithms. These are most + * commonly accessed via the {@link io.jsonwebtoken.Jwts#KEY} convenience alias when creating a JWE. For example: + *
+ * {@link Jwts#builder()}.
+ *     // ... etc ...
+ *     .encryptWith(rsaPublicKey, {@link Jwts#KEY}.RSA_OAEP, Jwts.ENC.A256GCM) // <--
+ *     .build()
+ *

Direct type-safe references as shown above are often better than calling {@link #get(String)} or + * {@link #find(String)} which can be susceptible to misspelled or otherwise invalid string values.

+ * + * @see #get() + * @see #get(String) + * @see #find(String) + * @see #values() + * @see KeyAlgorithm + * @since JJWT_RELEASE_VERSION + */ +public final class StandardKeyAlgorithms implements Registry> { + + private static final Registry> REGISTRY = + Classes.newInstance("io.jsonwebtoken.impl.security.StandardKeyAlgorithmsBridge"); + + private static final StandardKeyAlgorithms INSTANCE = new StandardKeyAlgorithms(); + + /** + * Returns this registry (a static singleton). + * + * @return this registry (a static singleton). + */ + public static StandardKeyAlgorithms get() { // named `get` to mimic java.util.function.Supplier + return INSTANCE; + } + + /** + * Key algorithm reflecting direct use of a shared symmetric key as the JWE AEAD encryption key, as defined + * by RFC 7518 (JWA), Section 4.5. This + * algorithm does not produce encrypted key ciphertext. + */ + public final KeyAlgorithm DIRECT = doGet("dir"); + + /** + * AES Key Wrap algorithm with default initial value using a 128-bit key, as defined by + * RFC 7518 (JWA), Section 4.4. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Encrypts this newly-generated {@code SecretKey} with a 128-bit shared symmetric key using the + * AES Key Wrap algorithm, producing encrypted key ciphertext.
  4. + *
  5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  6. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Decrypts the encrypted key ciphertext with the 128-bit shared symmetric key, + * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
  4. + *
  5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  6. + *
+ */ + public final SecretKeyAlgorithm A128KW = doGet("A128KW"); + + /** + * AES Key Wrap algorithm with default initial value using a 192-bit key, as defined by + * RFC 7518 (JWA), Section 4.4. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Encrypts this newly-generated {@code SecretKey} with a 192-bit shared symmetric key using the + * AES Key Wrap algorithm, producing encrypted key ciphertext.
  4. + *
  5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  6. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Decrypts the encrypted key ciphertext with the 192-bit shared symmetric key, + * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
  4. + *
  5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  6. + *
+ */ + public final SecretKeyAlgorithm A192KW = doGet("A192KW"); + + /** + * AES Key Wrap algorithm with default initial value using a 256-bit key, as defined by + * RFC 7518 (JWA), Section 4.4. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Encrypts this newly-generated {@code SecretKey} with a 256-bit shared symmetric key using the + * AES Key Wrap algorithm, producing encrypted key ciphertext.
  4. + *
  5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  6. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Decrypts the encrypted key ciphertext with the 256-bit shared symmetric key, + * using the AES Key Unwrap algorithm, producing the decryption key plaintext.
  4. + *
  5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  6. + *
+ */ + public final SecretKeyAlgorithm A256KW = doGet("A256KW"); + + /** + * Key wrap algorithm with AES GCM using a 128-bit key, as defined by + * RFC 7518 (JWA), Section 4.7. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Generates a new secure-random 96-bit Initialization Vector to use during key wrap/encryption.
  4. + *
  5. Encrypts this newly-generated {@code SecretKey} with a 128-bit shared symmetric key using the + * AES GCM Key Wrap algorithm with the generated Initialization Vector, producing encrypted key ciphertext + * and GCM authentication tag.
  6. + *
  7. Sets the generated initialization vector as the required + * "iv" + * (Initialization Vector) Header Parameter
  8. + *
  9. Sets the resulting GCM authentication tag as the required + * "tag" + * (Authentication Tag) Header Parameter
  10. + *
  11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  12. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Obtains the required initialization vector from the + * "iv" + * (Initialization Vector) Header Parameter
  4. + *
  5. Obtains the required GCM authentication tag from the + * "tag" + * (Authentication Tag) Header Parameter
  6. + *
  7. Decrypts the encrypted key ciphertext with the 128-bit shared symmetric key, the initialization vector + * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key + * plaintext.
  8. + *
  9. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  10. + *
+ */ + public final SecretKeyAlgorithm A128GCMKW = doGet("A128GCMKW"); + + /** + * Key wrap algorithm with AES GCM using a 192-bit key, as defined by + * RFC 7518 (JWA), Section 4.7. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Generates a new secure-random 96-bit Initialization Vector to use during key wrap/encryption.
  4. + *
  5. Encrypts this newly-generated {@code SecretKey} with a 192-bit shared symmetric key using the + * AES GCM Key Wrap algorithm with the generated Initialization Vector, producing encrypted key ciphertext + * and GCM authentication tag.
  6. + *
  7. Sets the generated initialization vector as the required + * "iv" + * (Initialization Vector) Header Parameter
  8. + *
  9. Sets the resulting GCM authentication tag as the required + * "tag" + * (Authentication Tag) Header Parameter
  10. + *
  11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  12. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Obtains the required initialization vector from the + * "iv" + * (Initialization Vector) Header Parameter
  4. + *
  5. Obtains the required GCM authentication tag from the + * "tag" + * (Authentication Tag) Header Parameter
  6. + *
  7. Decrypts the encrypted key ciphertext with the 192-bit shared symmetric key, the initialization vector + * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key \ + * plaintext.
  8. + *
  9. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  10. + *
+ */ + public final SecretKeyAlgorithm A192GCMKW = doGet("A192GCMKW"); + + /** + * Key wrap algorithm with AES GCM using a 256-bit key, as defined by + * RFC 7518 (JWA), Section 4.7. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Generates a new secure-random 96-bit Initialization Vector to use during key wrap/encryption.
  4. + *
  5. Encrypts this newly-generated {@code SecretKey} with a 256-bit shared symmetric key using the + * AES GCM Key Wrap algorithm with the generated Initialization Vector, producing encrypted key ciphertext + * and GCM authentication tag.
  6. + *
  7. Sets the generated initialization vector as the required + * "iv" + * (Initialization Vector) Header Parameter
  8. + *
  9. Sets the resulting GCM authentication tag as the required + * "tag" + * (Authentication Tag) Header Parameter
  10. + *
  11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  12. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Obtains the required initialization vector from the + * "iv" + * (Initialization Vector) Header Parameter
  4. + *
  5. Obtains the required GCM authentication tag from the + * "tag" + * (Authentication Tag) Header Parameter
  6. + *
  7. Decrypts the encrypted key ciphertext with the 256-bit shared symmetric key, the initialization vector + * and GCM authentication tag using the AES GCM Key Unwrap algorithm, producing the decryption key \ + * plaintext.
  8. + *
  9. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  10. + *
+ */ + public final SecretKeyAlgorithm A256GCMKW = doGet("A256GCMKW"); + + /** + * Key encryption algorithm using PBES2 with HMAC SHA-256 and "A128KW" wrapping + * as defined by + * RFC 7518 (JWA), Section 4.8. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Determines the number of PBDKF2 iterations via the JWE header's + * {@link JweHeader#getPbes2Count() pbes2Count} value. If that value is not set, a suitable number of + * iterations will be chosen based on + * OWASP + * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
  2. + *
  3. Generates a new secure-random salt input and sets it as the JWE header + * {@link JweHeader#getPbes2Salt() pbes2Salt} value.
  4. + *
  5. Derives a 128-bit Key Encryption Key with the PBES2-HS256 password-based key derivation algorithm, + * using the provided password, iteration count, and input salt as arguments.
  6. + *
  7. Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  8. + *
  9. Encrypts this newly-generated Content Encryption {@code SecretKey} with the {@code A128KW} key wrap + * algorithm using the 128-bit derived password-based Key Encryption Key from step {@code #3}, + * producing encrypted key ciphertext.
  10. + *
  11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * Content Encryption {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated + * {@link AeadAlgorithm}.
  12. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required PBKDF2 input salt from the + * "p2s" + * (PBES2 Salt Input) Header Parameter
  2. + *
  3. Obtains the required PBKDF2 iteration count from the + * "p2c" + * (PBES2 Count) Header Parameter
  4. + *
  5. Derives the 128-bit Key Encryption Key with the PBES2-HS256 password-based key derivation algorithm, + * using the provided password, obtained salt input, and obtained iteration count as arguments.
  6. + *
  7. Obtains the encrypted key ciphertext embedded in the received JWE.
  8. + *
  9. Decrypts the encrypted key ciphertext with with the {@code A128KW} key unwrap + * algorithm using the 128-bit derived password-based Key Encryption Key from step {@code #3}, + * producing the decryption key plaintext.
  10. + *
  11. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  12. + *
+ */ + public final KeyAlgorithm PBES2_HS256_A128KW = doGet("PBES2-HS256+A128KW"); + + /** + * Key encryption algorithm using PBES2 with HMAC SHA-384 and "A192KW" wrapping + * as defined by + * RFC 7518 (JWA), Section 4.8. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Determines the number of PBDKF2 iterations via the JWE header's + * {@link JweHeader#getPbes2Count() pbes2Count} value. If that value is not set, a suitable number of + * iterations will be chosen based on + * OWASP + * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
  2. + *
  3. Generates a new secure-random salt input and sets it as the JWE header + * {@link JweHeader#getPbes2Salt() pbes2Salt} value.
  4. + *
  5. Derives a 192-bit Key Encryption Key with the PBES2-HS384 password-based key derivation algorithm, + * using the provided password, iteration count, and input salt as arguments.
  6. + *
  7. Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  8. + *
  9. Encrypts this newly-generated Content Encryption {@code SecretKey} with the {@code A192KW} key wrap + * algorithm using the 192-bit derived password-based Key Encryption Key from step {@code #3}, + * producing encrypted key ciphertext.
  10. + *
  11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * Content Encryption {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated + * {@link AeadAlgorithm}.
  12. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required PBKDF2 input salt from the + * "p2s" + * (PBES2 Salt Input) Header Parameter
  2. + *
  3. Obtains the required PBKDF2 iteration count from the + * "p2c" + * (PBES2 Count) Header Parameter
  4. + *
  5. Derives the 192-bit Key Encryption Key with the PBES2-HS384 password-based key derivation algorithm, + * using the provided password, obtained salt input, and obtained iteration count as arguments.
  6. + *
  7. Obtains the encrypted key ciphertext embedded in the received JWE.
  8. + *
  9. Decrypts the encrypted key ciphertext with with the {@code A192KW} key unwrap + * algorithm using the 192-bit derived password-based Key Encryption Key from step {@code #3}, + * producing the decryption key plaintext.
  10. + *
  11. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  12. + *
+ */ + public final KeyAlgorithm PBES2_HS384_A192KW = doGet("PBES2-HS384+A192KW"); + + /** + * Key encryption algorithm using PBES2 with HMAC SHA-512 and "A256KW" wrapping + * as defined by + * RFC 7518 (JWA), Section 4.8. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Determines the number of PBDKF2 iterations via the JWE header's + * {@link JweHeader#getPbes2Count() pbes2Count} value. If that value is not set, a suitable number of + * iterations will be chosen based on + * OWASP + * PBKDF2 recommendations and then that value is set as the JWE header {@code pbes2Count} value.
  2. + *
  3. Generates a new secure-random salt input and sets it as the JWE header + * {@link JweHeader#getPbes2Salt() pbes2Salt} value.
  4. + *
  5. Derives a 256-bit Key Encryption Key with the PBES2-HS512 password-based key derivation algorithm, + * using the provided password, iteration count, and input salt as arguments.
  6. + *
  7. Generates a new secure-random Content Encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  8. + *
  9. Encrypts this newly-generated Content Encryption {@code SecretKey} with the {@code A256KW} key wrap + * algorithm using the 256-bit derived password-based Key Encryption Key from step {@code #3}, + * producing encrypted key ciphertext.
  10. + *
  11. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * Content Encryption {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated + * {@link AeadAlgorithm}.
  12. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required PBKDF2 input salt from the + * "p2s" + * (PBES2 Salt Input) Header Parameter
  2. + *
  3. Obtains the required PBKDF2 iteration count from the + * "p2c" + * (PBES2 Count) Header Parameter
  4. + *
  5. Derives the 256-bit Key Encryption Key with the PBES2-HS512 password-based key derivation algorithm, + * using the provided password, obtained salt input, and obtained iteration count as arguments.
  6. + *
  7. Obtains the encrypted key ciphertext embedded in the received JWE.
  8. + *
  9. Decrypts the encrypted key ciphertext with with the {@code A256KW} key unwrap + * algorithm using the 256-bit derived password-based Key Encryption Key from step {@code #3}, + * producing the decryption key plaintext.
  10. + *
  11. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  12. + *
+ */ + public final KeyAlgorithm PBES2_HS512_A256KW = doGet("PBES2-HS512+A256KW"); + + /** + * Key Encryption with {@code RSAES-PKCS1-v1_5}, as defined by + * RFC 7518 (JWA), Section 4.2. + * This algorithm requires a key size of 2048 bits or larger. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Encrypts this newly-generated {@code SecretKey} with the RSA key wrap algorithm, using the JWE + * recipient's RSA Public Key, producing encrypted key ciphertext.
  4. + *
  5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  6. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Receives the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Decrypts the encrypted key ciphertext with the RSA key unwrap algorithm, using the JWE recipient's + * RSA Private Key, producing the decryption key plaintext.
  4. + *
  5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  6. + *
+ */ + public final KeyAlgorithm RSA1_5 = doGet("RSA1_5"); + + /** + * Key Encryption with {@code RSAES OAEP using default parameters}, as defined by + * RFC 7518 (JWA), Section 4.3. + * This algorithm requires a key size of 2048 bits or larger. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Encrypts this newly-generated {@code SecretKey} with the RSA OAEP with SHA-1 and MGF1 key wrap algorithm, + * using the JWE recipient's RSA Public Key, producing encrypted key ciphertext.
  4. + *
  5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  6. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Receives the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Decrypts the encrypted key ciphertext with the RSA OAEP with SHA-1 and MGF1 key unwrap algorithm, + * using the JWE recipient's RSA Private Key, producing the decryption key plaintext.
  4. + *
  5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  6. + *
+ */ + public final KeyAlgorithm RSA_OAEP = doGet("RSA-OAEP"); + + /** + * Key Encryption with {@code RSAES OAEP using SHA-256 and MGF1 with SHA-256}, as defined by + * RFC 7518 (JWA), Section 4.3. + * This algorithm requires a key size of 2048 bits or larger. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  2. + *
  3. Encrypts this newly-generated {@code SecretKey} with the RSA OAEP with SHA-256 and MGF1 key wrap + * algorithm, using the JWE recipient's RSA Public Key, producing encrypted key ciphertext.
  4. + *
  5. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  6. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Receives the encrypted key ciphertext embedded in the received JWE.
  2. + *
  3. Decrypts the encrypted key ciphertext with the RSA OAEP with SHA-256 and MGF1 key unwrap algorithm, + * using the JWE recipient's RSA Private Key, producing the decryption key plaintext.
  4. + *
  5. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  6. + *
+ */ + public final KeyAlgorithm RSA_OAEP_256 = doGet("RSA-OAEP-256"); + + /** + * Key Agreement with {@code ECDH-ES using Concat KDF} as defined by + * RFC 7518 (JWA), Section 4.6. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
  2. + *
  3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
  4. + *
  5. Derives a symmetric Content + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  6. + *
  7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
  8. + *
  9. Returns the derived symmetric {@code SecretKey} for JJWT to use to encrypt the entire JWE with the + * associated {@link AeadAlgorithm}. Encrypted key ciphertext is not produced with this algorithm, so + * the resulting JWE will not contain any embedded key ciphertext.
  10. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
  2. + *
  3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  4. + *
  5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
  6. + *
  7. Derives the symmetric Content + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  8. + *
  9. Returns the derived symmetric {@code SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  10. + *
+ */ + public final KeyAlgorithm ECDH_ES = doGet("ECDH-ES"); + + /** + * Key Agreement with Key Wrapping via + * ECDH-ES using Concat KDF and CEK wrapped with "A128KW" as defined by + * RFC 7518 (JWA), Section 4.6. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
  2. + *
  3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
  4. + *
  5. Derives a 128-bit symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  6. + *
  7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
  8. + *
  9. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  10. + *
  11. Encrypts this newly-generated {@code SecretKey} with the {@code A128KW} key wrap + * algorithm using the derived symmetric Key Encryption Key from step {@code #3}, producing encrypted key ciphertext.
  12. + *
  13. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  14. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
  2. + *
  3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  4. + *
  5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
  6. + *
  7. Derives the symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  8. + *
  9. Obtains the encrypted key ciphertext embedded in the received JWE.
  10. + *
  11. Decrypts the encrypted key ciphertext with the AES Key Unwrap algorithm using the + * 128-bit derived symmetric key from step {@code #4}, producing the decryption key plaintext.
  12. + *
  13. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  14. + *
+ */ + public final KeyAlgorithm ECDH_ES_A128KW = doGet("ECDH-ES+A128KW"); + + /** + * Key Agreement with Key Wrapping via + * ECDH-ES using Concat KDF and CEK wrapped with "A192KW" as defined by + * RFC 7518 (JWA), Section 4.6. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
  2. + *
  3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
  4. + *
  5. Derives a 192-bit symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  6. + *
  7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
  8. + *
  9. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  10. + *
  11. Encrypts this newly-generated {@code SecretKey} with the {@code A192KW} key wrap + * algorithm using the derived symmetric Key Encryption Key from step {@code #3}, producing encrypted key + * ciphertext.
  12. + *
  13. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  14. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
  2. + *
  3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  4. + *
  5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
  6. + *
  7. Derives the 192-bit symmetric + * Key Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  8. + *
  9. Obtains the encrypted key ciphertext embedded in the received JWE.
  10. + *
  11. Decrypts the encrypted key ciphertext with the AES Key Unwrap algorithm using the + * 192-bit derived symmetric key from step {@code #4}, producing the decryption key plaintext.
  12. + *
  13. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  14. + *
+ */ + public final KeyAlgorithm ECDH_ES_A192KW = doGet("ECDH-ES+A192KW"); + + /** + * Key Agreement with Key Wrapping via + * ECDH-ES using Concat KDF and CEK wrapped with "A256KW" as defined by + * RFC 7518 (JWA), Section 4.6. + * + *

During JWE creation, this algorithm:

+ *
    + *
  1. Generates a new secure-random Elliptic Curve public/private key pair on the same curve as the + * JWE recipient's EC Public Key.
  2. + *
  3. Generates a shared secret with the ECDH key agreement algorithm using the generated EC Private Key + * and the JWE recipient's EC Public Key.
  4. + *
  5. Derives a 256-bit symmetric Key + * Encryption {@code SecretKey} with the Concat KDF algorithm using the + * generated shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  6. + *
  7. Sets the generated EC key pair's Public Key as the required + * "epk" + * (Ephemeral Public Key) Header Parameter to be transmitted in the JWE.
  8. + *
  9. Generates a new secure-random content encryption {@link SecretKey} suitable for use with a + * specified {@link AeadAlgorithm} (using {@link AeadAlgorithm#keyBuilder()}).
  10. + *
  11. Encrypts this newly-generated {@code SecretKey} with the {@code A256KW} key wrap + * algorithm using the derived symmetric Key Encryption Key from step {@code #3}, producing encrypted key + * ciphertext.
  12. + *
  13. Returns the encrypted key ciphertext for inclusion in the final JWE as well as the newly-generated + * {@code SecretKey} for JJWT to use to encrypt the entire JWE with associated {@link AeadAlgorithm}.
  14. + *
+ *

For JWE decryption, this algorithm:

+ *
    + *
  1. Obtains the required ephemeral Elliptic Curve Public Key from the + * "epk" + * (Ephemeral Public Key) Header Parameter.
  2. + *
  3. Validates that the ephemeral Public Key is on the same curve as the recipient's EC Private Key.
  4. + *
  5. Obtains the shared secret with the ECDH key agreement algorithm using the obtained EC Public Key + * and the JWE recipient's EC Private Key.
  6. + *
  7. Derives the 256-bit symmetric + * Key Encryption {@code SecretKey} with the Concat KDF algorithm using the + * obtained shared secret and any available + * {@link JweHeader#getAgreementPartyUInfo() PartyUInfo} and + * {@link JweHeader#getAgreementPartyVInfo() PartyVInfo}.
  8. + *
  9. Obtains the encrypted key ciphertext embedded in the received JWE.
  10. + *
  11. Decrypts the encrypted key ciphertext with the AES Key Unwrap algorithm using the + * 256-bit derived symmetric key from step {@code #4}, producing the decryption key plaintext.
  12. + *
  13. Returns the decryption key plaintext as a {@link SecretKey} for JJWT to use to decrypt the entire + * JWE using the JWE's identified "enc" {@link AeadAlgorithm}.
  14. + *
+ */ + public final KeyAlgorithm ECDH_ES_A256KW = doGet("ECDH-ES+A256KW"); + + //prevent instantiation + private StandardKeyAlgorithms() { + } + + // do not change this visibility. Raw type method signature not be publicly exposed + @SuppressWarnings("unchecked") + private T doGet(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return (T) get(id); + } + + /** + * Returns all JWA-standard + * Key Management Algorithms as an + * unmodifiable collection. + * + * @return all JWA-standard + * Key Management Algorithms as an + * unmodifiable collection. + */ + public Collection> values() { + return REGISTRY.values(); + } + + /** + * Returns the JWE Key Management Algorithm with the specified + * {@code alg} key algorithm identifier or + * {@code null} if an algorithm for the specified {@code id} cannot be found. If a JWA-standard + * instance must be resolved, consider using the {@link #get(String)} method instead. + * + * @param id a JWA standard {@code alg} key algorithm identifier + * @return the associated KeyAlgorithm instance or {@code null} otherwise. + * @see RFC 7518, Section 4.1 + * @see #get(String) + */ + public KeyAlgorithm find(String id) { + return REGISTRY.find(id); + } + + /** + * Returns the JWE Key Management Algorithm with the specified + * {@code alg} key algorithm identifier or + * throws an {@link IllegalArgumentException} if there is no JWE-standard algorithm for the specified + * {@code id}. If a JWE-standard instance result is not mandatory, consider using the {@link #find(String)} + * method instead. + * + * @param id a JWA standard {@code alg} key algorithm identifier + * @return the associated {@code KeyAlgorithm} instance. + * @throws IllegalArgumentException if there is no JWA-standard algorithm for the specified identifier. + * @see #find(String) + * @see RFC 7518, Section 4.1 + */ + public KeyAlgorithm get(String id) throws IllegalArgumentException { + return REGISTRY.get(id); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/StandardSecureDigestAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/StandardSecureDigestAlgorithms.java new file mode 100644 index 00000000..45df0238 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/StandardSecureDigestAlgorithms.java @@ -0,0 +1,254 @@ +/* + * 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.security; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Registry; + +import java.security.Key; +import java.util.Collection; + +/** + * Registry of all standard JWS + * Cryptographic Algorithms for Digital + * Signatures and MACs. These are most commonly accessed via the {@link io.jsonwebtoken.Jwts#SIG} convenience + * alias when creating a JWS. For example: + *
+ * {@link Jwts#builder()}.
+ *     // ... etc ...
+ *     .{@link io.jsonwebtoken.JwtBuilder#signWith(Key, SecureDigestAlgorithm) signWith}(aKey, {@link Jwts#SIG}.HS256) // <--
+ *     .build()
+ * + * @see #get() + * @see #get(String) + * @see #find(String) + * @see #values() + * @since JJWT_RELEASE_VERSION + */ +public final class StandardSecureDigestAlgorithms implements Registry> { + + private static final Registry> IMPL = + Classes.newInstance("io.jsonwebtoken.impl.security.StandardSecureDigestAlgorithmsBridge"); + + private static final StandardSecureDigestAlgorithms INSTANCE = new StandardSecureDigestAlgorithms(); + + /** + * Returns this registry (a static singleton). + * + * @return this registry (a static singleton). + */ + public static StandardSecureDigestAlgorithms get() { // named `get` to mimic java.util.function.Supplier + return INSTANCE; + } + + /** + * The "none" signature algorithm as defined by + * RFC 7518, Section 3.6. This algorithm + * is used only when creating unsecured (not integrity protected) JWSs and is not usable in any other scenario. + * Any attempt to call its methods will result in an exception being thrown. + */ + public final SecureDigestAlgorithm NONE = doGet("none"); + + /** + * {@code HMAC using SHA-256} message authentication algorithm as defined by + * RFC 7518, Section 3.2. This algorithm + * requires a 256-bit (32 byte) key. + */ + public final MacAlgorithm HS256 = doGet("HS256"); + + /** + * {@code HMAC using SHA-384} message authentication algorithm as defined by + * RFC 7518, Section 3.2. This algorithm + * requires a 384-bit (48 byte) key. + */ + public final MacAlgorithm HS384 = doGet("HS384"); + + /** + * {@code HMAC using SHA-512} message authentication algorithm as defined by + * RFC 7518, Section 3.2. This algorithm + * requires a 512-bit (64 byte) key. + */ + public final MacAlgorithm HS512 = doGet("HS512"); + + /** + * {@code RSASSA-PKCS1-v1_5 using SHA-256} signature algorithm as defined by + * RFC 7518, Section 3.3. This algorithm + * requires a 2048-bit key. + */ + public final SignatureAlgorithm RS256 = doGet("RS256"); + + /** + * {@code RSASSA-PKCS1-v1_5 using SHA-384} signature algorithm as defined by + * RFC 7518, Section 3.3. This algorithm + * requires a 2048-bit key, but the JJWT team recommends a 3072-bit key. + */ + public final SignatureAlgorithm RS384 = doGet("RS384"); + + /** + * {@code RSASSA-PKCS1-v1_5 using SHA-512} signature algorithm as defined by + * RFC 7518, Section 3.3. This algorithm + * requires a 2048-bit key, but the JJWT team recommends a 4096-bit key. + */ + public final SignatureAlgorithm RS512 = doGet("RS512"); + + /** + * {@code RSASSA-PSS using SHA-256 and MGF1 with SHA-256} signature algorithm as defined by + * RFC 7518, Section 3.51. + * This algorithm requires a 2048-bit key. + * + *

1 Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

+ */ + public final SignatureAlgorithm PS256 = doGet("PS256"); + + /** + * {@code RSASSA-PSS using SHA-384 and MGF1 with SHA-384} signature algorithm as defined by + * RFC 7518, Section 3.51. + * This algorithm requires a 2048-bit key, but the JJWT team recommends a 3072-bit key. + * + *

1 Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

+ */ + public final SignatureAlgorithm PS384 = doGet("PS384"); + + /** + * {@code RSASSA-PSS using SHA-512 and MGF1 with SHA-512} signature algorithm as defined by + * RFC 7518, Section 3.51. + * This algorithm requires a 2048-bit key, but the JJWT team recommends a 4096-bit key. + * + *

1 Requires Java 11 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath. If on Java 10 or earlier, BouncyCastle will be used automatically if found in the runtime + * classpath.

+ */ + public final SignatureAlgorithm PS512 = doGet("PS512"); + + /** + * {@code ECDSA using P-256 and SHA-256} signature algorithm as defined by + * RFC 7518, Section 3.4. This algorithm + * requires a 256-bit key. + */ + public final SignatureAlgorithm ES256 = doGet("ES256"); + + /** + * {@code ECDSA using P-384 and SHA-384} signature algorithm as defined by + * RFC 7518, Section 3.4. This algorithm + * requires a 384-bit key. + */ + public final SignatureAlgorithm ES384 = doGet("ES384"); + + /** + * {@code ECDSA using P-521 and SHA-512} signature algorithm as defined by + * RFC 7518, Section 3.4. This algorithm + * requires a 521-bit key. + */ + public final SignatureAlgorithm ES512 = doGet("ES512"); + + /** + * {@code EdDSA} signature algorithm as defined by + * RFC 8037, Section 3.1. This algorithm + * requires either {@code Ed25519} or {@code Ed448} Edwards Curve keys. + *

This algorithm requires at least JDK 15 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

+ */ + public final SignatureAlgorithm EdDSA = doGet("EdDSA"); + + /** + * {@code EdDSA} signature algorithm using Curve {@code Ed25519} as defined by + * RFC 8037, Section 3.1. This algorithm + * requires {@code Ed25519} Edwards Curve keys to create signatures. This is a convenience alias for + * {@link #EdDSA} that defaults key generation to {@code Ed25519} keys. + *

This algorithm requires at least JDK 15 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

+ */ + public final SignatureAlgorithm Ed25519 = doGet("Ed25519"); + + /** + * {@code EdDSA} signature algorithm using Curve {@code Ed448} as defined by + * RFC 8037, Section 3.1. This algorithm + * requires {@code Ed448} Edwards Curve keys to create signatures. This is a convenience alias for + * {@link #EdDSA} that defaults key generation to {@code Ed448} keys. + *

This algorithm requires at least JDK 15 or a compatible JCA Provider (like BouncyCastle) in the runtime + * classpath.

+ */ + public final SignatureAlgorithm Ed448 = doGet("Ed448"); + + /** + * Prevent external instantiation. + */ + private StandardSecureDigestAlgorithms() { + } + + // do not change this visibility. Raw type method signature not be publicly exposed + @SuppressWarnings("unchecked") + private T doGet(String id) { + Assert.hasText(id, "id cannot be null or empty."); + return (T) get(id); + } + + /** + * Returns all standard JWS + * Digital Signature and MAC Algorithms + * as an unmodifiable collection. + * + * @return all standard JWS + * Digital Signature and MAC Algorithms + * as an unmodifiable collection. + */ + public Collection> values() { + return IMPL.values(); + } + + /** + * Returns the {@link SignatureAlgorithm} or {@link MacAlgorithm} instance with the specified + * {@code alg} algorithm identifier or + * {@code null} if an algorithm for the specified {@code id} cannot be found. If a JWA-standard + * instance must be resolved, consider using the {@link #get(String)} method instead. + * + * @param id a JWA-standard identifier defined in + * JWA RFC 7518, Section 3.1 + * in the "alg" Param Value column. + * @return the {@code SecureDigestAlgorithm} instance with the specified JWA-standard identifier, or + * {@code null} if no algorithm with that identifier exists. + * @see RFC 7518, Section 3.1 + * @see #get(String) + */ + public SecureDigestAlgorithm find(String id) { + return IMPL.find(id); + } + + /** + * Returns the {@link SignatureAlgorithm} or {@link MacAlgorithm} instance with the specified + * {@code alg} algorithm identifier or + * throws an {@link IllegalArgumentException} if there is no JWA-standard algorithm for the specified + * {@code id}. If a JWA-standard instance result is not mandatory, consider using the {@link #find(String)} + * method instead. + * + * @param id a JWA-standard identifier defined in + * JWA RFC 7518, Section 3.1 + * in the "alg" Param Value column. + * @return the associated {@code SecureDigestAlgorithm} instance. + * @throws IllegalArgumentException if there is no JWA-standard algorithm for the specified identifier. + * @see RFC 7518, Section 3.1 + * @see #find(String) + */ + public SecureDigestAlgorithm get(String id) throws IllegalArgumentException { + return IMPL.get(id); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java new file mode 100644 index 00000000..9b0447f4 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java @@ -0,0 +1,43 @@ +/* + * 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.security; + +/** + * Exception thrown when encountering a key or key material that is not supported or recognized. + * + * @since JJWT_RELEASE_VERSION + */ +public class UnsupportedKeyException extends InvalidKeyException { + + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ + public UnsupportedKeyException(String message) { + super(message); + } + + /** + * Creates a new instance with the specified explanation message and underlying cause. + * + * @param msg the message explaining why the exception is thrown. + * @param cause the underlying cause that resulted in this exception being thrown. + */ + public UnsupportedKeyException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifyDigestRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifyDigestRequest.java new file mode 100644 index 00000000..dc84c5d0 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/VerifyDigestRequest.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2022 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.security; + +/** + * 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}. + * + *

Secure digest algorithms that use keys to perform + * digital signature or + * message + * authentication code verification will use {@link VerifySecureDigestRequest} instead.

+ * + * @see VerifySecureDigestRequest + * @since JJWT_RELEASE_VERSION + */ +public interface VerifyDigestRequest extends Request, DigestSupplier { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifySecureDigestRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifySecureDigestRequest.java new file mode 100644 index 00000000..c73a1ec8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/VerifySecureDigestRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2022 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.security; + +import java.security.Key; + +/** + * A request to a {@link SecureDigestAlgorithm} to verify a previously-computed + * digital signature or + * message + * authentication code. + * + *

The content to verify will be available via {@link #getPayload()}, the previously-computed signature or MAC will + * be available via {@link #getDigest()}, and the verification key will be available via {@link #getKey()}.

+ * + * @param the type of {@link Key} used to verify a digital signature or message authentication code + * @since JJWT_RELEASE_VERSION + */ +public interface VerifySecureDigestRequest extends SecureRequest, VerifyDigestRequest { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java index 8a7688fe..8b466d04 100644 --- a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java +++ b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java @@ -16,10 +16,18 @@ package io.jsonwebtoken.security; /** + * Exception thrown when encountering a key that is not strong enough (of sufficient length) to be used with + * a particular algorithm or in a particular security context. + * * @since 0.10.0 */ public class WeakKeyException extends InvalidKeyException { + /** + * Creates a new instance with the specified explanation message. + * + * @param message the message explaining why the exception is thrown. + */ public WeakKeyException(String message) { super(message); } diff --git a/api/src/main/java/io/jsonwebtoken/security/X509Accessor.java b/api/src/main/java/io/jsonwebtoken/security/X509Accessor.java new file mode 100644 index 00000000..1efe835c --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/X509Accessor.java @@ -0,0 +1,135 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Accessor methods of X.509-specific properties of an associated JWT Header or JWK, guaranteeing consistent behavior + * across similar but distinct JWT concepts with identical parameter names. + * + * @since JJWT_RELEASE_VERSION + */ +public interface X509Accessor { + + /** + * Returns the {@code x5u} (X.509 URL) that refers to a resource for the associated X.509 public key certificate + * or certificate chain, or {@code null} if not present. + * + *

When present, the URI MUST refer to a resource for an X.509 public key certificate or certificate + * chain that conforms to RFC 5280 in PEM-encoded form, + * with each certificate delimited as specified in + * Section 6.1 of RFC 4945. + * The key in the first certificate MUST match the public key represented by other members of the + * associated JWT or JWK. The protocol used to acquire the resource MUST provide integrity protection; + * an HTTP GET request to retrieve the certificate MUST use + * HTTP over TLS; the identity of the server + * MUST be validated, as per + * Section 6 of RFC 6125.

+ * + *
    + *
  • When present in a {@link JwsHeader}, the certificate or first certificate in the chain corresponds + * the public key complement of the private key used to digitally sign the JWS.
  • + *
  • When present in a {@link JweHeader}, the certificate or certificate chain corresponds to the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to + * decrypt the JWE.
  • + *
  • When present in an {@link AsymmetricJwk}, the certificate or first certificate in the chain + * MUST contain the public key represented by the JWK.
  • + *
+ * + * @return the {@code x5u} (X.509 URL) that refers to a resource for the associated X.509 public key certificate or + * certificate chain. + * @see JWK {@code x5u} (X.509 URL) Parameter + * @see JWS {@code x5u} (X.509 URL) Header Parameter + * @see JWE {@code x5u} (X.509 URL) Header Parameter + */ + URI getX509Url(); + + /** + * Returns the associated {@code x5c} (X.509 Certificate Chain), or {@code null} if not present. The initial + * certificate MAY be followed by additional certificates, with each subsequent certificate being the + * one used to certify the previous one. + * + *
    + *
  • When present in a {@link JwsHeader}, the first certificate (at list index 0) MUST contain + * the public key complement of the private key used to digitally sign the JWS.
  • + *
  • When present in a {@link JweHeader}, the first certificate (at list index 0) MUST contain + * the public key to which the JWE was encrypted, and may be used to determine the private key needed to + * decrypt the JWE.
  • + *
  • When present in an {@link AsymmetricJwk}, the first certificate (at list index 0) + * MUST contain the public key represented by the JWK.
  • + *
+ * + * @return the associated {@code x5c} (X.509 Certificate Chain), or {@code null} if not present. + * @see JWK x5c (X.509 Certificate Chain) Parameter + * @see JWS x5c (X.509 Certificate Chain) Header Parameter + * @see JWE x5c (X.509 Certificate Chain) Header Parameter + */ + List getX509CertificateChain(); + + /** + * Returns the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * associated X.509 Certificate, or {@code null} if not present. + * + *

Note that certificate thumbprints are also sometimes known as certificate fingerprints.

+ * + *
    + *
  • When present in a {@link JwsHeader}, it is the SHA-1 thumbprint of the X.509 certificate complement + * of the private key used to digitally sign the JWS.
  • + *
  • When present in a {@link JweHeader}, it is the SHA-1 thumbprint of the X.509 Certificate containing + * the public key to which the JWE was encrypted, and may be used to determine the private key + * needed to decrypt the JWE.
  • + *
  • When present in an {@link AsymmetricJwk}, it is the SHA-1 thumbprint of the X.509 certificate + * containing the public key represented by the JWK.
  • + *
+ * + * @return the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * associated X.509 Certificate, or {@code null} if not present + * @see JWK x5t (X.509 Certificate SHA-1 Thumbprint) Parameter + * @see JWS x5t (X.509 Certificate SHA-1 Thumbprint) Header Parameter + * @see JWE x5t (X.509 Certificate SHA-1 Thumbprint) Header Parameter + */ + byte[] getX509CertificateSha1Thumbprint(); + + /** + * Returns the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * associated X.509 Certificate, or {@code null} if not present. + * + *

Note that certificate thumbprints are also sometimes known as certificate fingerprints.

+ * + *
    + *
  • When present in a {@link JwsHeader}, it is the SHA-256 thumbprint of the X.509 certificate complement + * of the private key used to digitally sign the JWS.
  • + *
  • When present in a {@link JweHeader}, it is the SHA-256 thumbprint of the X.509 Certificate containing + * the public key to which the JWE was encrypted, and may be used to determine the private key + * needed to decrypt the JWE.
  • + *
  • When present in an {@link AsymmetricJwk}, it is the SHA-256 thumbprint of the X.509 certificate + * containing the public key represented by the JWK.
  • + *
+ * + * @return the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * associated X.509 Certificate, or {@code null} if not present + * @see JWK x5t#S256 (X.509 Certificate SHA-256 Thumbprint) Parameter + * @see JWS x5t#S256 (X.509 Certificate SHA-256 Thumbprint) Header Parameter + * @see JWE x5t#S256 (X.509 Certificate SHA-256 Thumbprint) Header Parameter + */ + byte[] getX509CertificateSha256Thumbprint(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/X509Builder.java b/api/src/main/java/io/jsonwebtoken/security/X509Builder.java new file mode 100644 index 00000000..99a7551d --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/X509Builder.java @@ -0,0 +1,56 @@ +/* + * 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.security; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Additional X.509-specific builder methods for constructing an associated JWT Header or JWK, enabling method chaining. + * + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface X509Builder> extends X509Mutator { + + /** + * If the {@code enable} argument is {@code true}, compute the SHA-1 thumbprint of the first + * {@link X509Certificate} in the configured {@link #setX509CertificateChain(List) x509CertificateChain}, and set + * the resulting value as the {@link #setX509CertificateSha1Thumbprint(byte[])} parameter. + * + *

If no chain has been configured, or {@code enable} is {@code false}, the builder will not compute nor add a + * {@code x5t} value.

+ * + * @param enable whether to compute the SHA-1 thumbprint on the first available X.509 Certificate and set + * the resulting value as the {@code x5t} value. + * @return the builder for method chaining. + */ + T withX509Sha1Thumbprint(boolean enable); + + /** + * If the {@code enable} argument is {@code true}, compute the SHA-256 thumbprint of the first + * {@link X509Certificate} in the configured {@link #setX509CertificateChain(List) x509CertificateChain}, and set + * the resulting value as the {@link #setX509CertificateSha256Thumbprint(byte[])} parameter. + * + *

If no chain has been configured, or {@code enable} is {@code false}, the builder will not compute nor add a + * {@code x5t#S256} value.

+ * + * @param enable whether to compute the SHA-256 thumbprint on the first available X.509 Certificate and set + * the resulting value as the {@code x5t#S256} value. + * @return the builder for method chaining. + */ + T withX509Sha256Thumbprint(boolean enable); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/X509Mutator.java b/api/src/main/java/io/jsonwebtoken/security/X509Mutator.java new file mode 100644 index 00000000..2fa241f8 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/X509Mutator.java @@ -0,0 +1,141 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Mutation (modifications) of X.509-specific properties of an associated JWT Header or JWK, enabling method chaining. + * + * @param the mutator subtype, for method chaining + * @since JJWT_RELEASE_VERSION + */ +public interface X509Mutator> { + + /** + * Sets the {@code x5u} (X.509 URL) that refers to a resource containing the X.509 public key certificate or + * certificate chain of the associated JWT or JWK. A {@code null} value will remove the property from the JSON map. + * + *

The URI MUST refer to a resource for an X.509 public key certificate or certificate chain that + * conforms to RFC 5280 in PEM-encoded form, with + * each certificate delimited as specified in + * Section 6.1 of RFC 4945. + * The key in the first certificate MUST match the public key represented by other members of the + * associated JWT or JWK. The protocol used to acquire the resource MUST provide integrity protection; + * an HTTP GET request to retrieve the certificate MUST use + * HTTP over TLS; the identity of the server + * MUST be validated, as per + * Section 6 of RFC 6125.

+ * + *
    + *
  • When set for a {@link JwsHeader}, the certificate or first certificate in the chain contains + * the public key complement of the private key used to digitally sign the JWS.
  • + *
  • When set for {@link JweHeader}, the certificate or first certificate in the chain contains the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to + * decrypt the JWE.
  • + *
  • When set for an {@link AsymmetricJwk}, the certificate or first certificate in the chain + * MUST contain the public key represented by the JWK.
  • + *
+ * + * @param uri the {@code x5u} (X.509 URL) that refers to a resource for the X.509 public key certificate or + * certificate chain associated with the JWT or JWK. + * @return the mutator/builder for method chaining. + * @see JWK x5u (X.509 URL) Parameter + * @see JWS x5u (X.509 URL) Header Parameter + * @see JWE x5u (X.509 URL) Header Parameter + */ + T setX509Url(URI uri); + + /** + * Sets the {@code x5c} (X.509 Certificate Chain) of the associated JWT or JWK. A {@code null} value will remove the + * property from the JSON map. The initial certificate MAY be followed by additional certificates, with + * each subsequent certificate being the one used to certify the previous one. + * + *
    + *
  • When set for a {@link JwsHeader}, the first certificate (at list index 0) MUST contain + * the public key complement of the private key used to digitally sign the JWS.
  • + *
  • When set for {@link JweHeader}, the first certificate (at list index 0) MUST contain the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to + * decrypt the JWE.
  • + *
  • When set for an {@link AsymmetricJwk}, the first certificate (at list index 0) MUST contain + * the public key represented by the JWK.
  • + *
+ * + * @param chain the {@code x5c} (X.509 Certificate Chain) of the associated JWT or JWK. + * @return the header/builder for method chaining. + * @see JWK x5c (X.509 Certificate Chain) Parameter + * @see JWS x5c (X.509 Certificate Chain) Header Parameter + * @see JWE x5c (X.509 Certificate Chain) Header Parameter + */ + T setX509CertificateChain(List chain); + + /** + * Sets the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT or JWK. A {@code null} value will remove the + * property from the JSON map. + * + *

Note that certificate thumbprints are also sometimes known as certificate fingerprints.

+ * + *
    + *
  • When set for a {@link JwsHeader}, it is the SHA-1 thumbprint of the X.509 certificate complement of + * the private key used to digitally sign the JWS.
  • + *
  • When set for {@link JweHeader}, it is the thumbprint of the X.509 Certificate containing the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to + * decrypt the JWE.
  • + *
  • When set for an {@link AsymmetricJwk}, it is the thumbprint of the X.509 certificate containing the + * public key represented by the JWK.
  • + *
+ * + * @param thumbprint the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT or JWK + * @return the header for method chaining + * @see JWK x5t (X.509 Certificate SHA-1 Thumbprint) Parameter + * @see JWS x5t (X.509 Certificate SHA-1 Thumbprint) Header Parameter + * @see JWE x5t (X.509 Certificate SHA-1 Thumbprint) Header Parameter + */ + T setX509CertificateSha1Thumbprint(byte[] thumbprint); + + /** + * Sets the {@code x5t#S256} (X.509 Certificate SHA-256 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT or JWK. A {@code null} value will remove the + * property from the JSON map. + * + *

Note that certificate thumbprints are also sometimes known as certificate fingerprints.

+ * + *
    + *
  • When set for a {@link JwsHeader}, it is the SHA-256 thumbprint of the X.509 certificate complement + * of the private key used to digitally sign the JWS.
  • + *
  • When set for {@link JweHeader}, it is the SHA-256 thumbprint of the X.509 Certificate containing the + * public key to which the JWE was encrypted, and may be used to determine the private key needed to + * decrypt the JWE.
  • + *
  • When set for a {@link AsymmetricJwk}, it is the SHA-256 thumbprint of the X.509 certificate + * containing the public key represented by the JWK.
  • + *
+ * + * @param thumbprint the {@code x5t} (X.509 Certificate SHA-1 Thumbprint) (a.k.a. digest) of the DER-encoding of the + * X.509 Certificate associated with the JWT or JWK + * @return the header for method chaining + * @see JWK x5t#S256 (X.509 Certificate SHA-256 Thumbprint) Parameter + * @see JWS x5t#S256 (X.509 Certificate SHA-256 Thumbprint) Header Parameter + * @see JWE x5t#S256 (X.509 Certificate SHA-256 Thumbprint) Header Parameter + */ + T setX509CertificateSha256Thumbprint(byte[] thumbprint); +} diff --git a/api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy deleted file mode 100644 index 33411d35..00000000 --- a/api/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2014 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 - -import io.jsonwebtoken.lang.Classes -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import static org.easymock.EasyMock.createMock -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.junit.Assert.assertSame -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.verify - -@RunWith(PowerMockRunner.class) -@PrepareForTest([Classes, CompressionCodecs]) -class CompressionCodecsTest { - - @Test - void testStatics() { - - mockStatic(Classes) - - def deflate = createMock(CompressionCodec) - def gzip = createMock(CompressionCodec) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.DeflateCompressionCodec"))).andReturn(deflate) - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.GzipCompressionCodec"))).andReturn(gzip) - - replay Classes, deflate, gzip - - assertSame deflate, CompressionCodecs.DEFLATE - assertSame gzip, CompressionCodecs.GZIP - - verify Classes, deflate, gzip - - //test coverage for private constructor: - new CompressionCodecs() - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy index 664462b7..754585a9 100644 --- a/api/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy @@ -34,9 +34,7 @@ class IncorrectClaimExceptionTest { replay header, claims - def ex = new IncorrectClaimException(header, claims, msg) - ex.setClaimName(claimName) - ex.setClaimValue(claimValue) + def ex = new IncorrectClaimException(header, claims, claimName, claimValue, msg) verify header, claims @@ -59,9 +57,7 @@ class IncorrectClaimExceptionTest { replay header, claims - def ex = new IncorrectClaimException(header, claims, msg, cause) - ex.setClaimName(claimName) - ex.setClaimValue(claimValue) + def ex = new IncorrectClaimException(header, claims, claimName, claimValue, msg, cause) verify header, claims diff --git a/api/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy index 5fbcf6e7..e8d42a86 100644 --- a/api/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy @@ -35,9 +35,7 @@ class InvalidClaimExceptionTest { replay header, claims - def ex = new InvalidClaimException(header, claims, msg, cause) - ex.setClaimName(claimName) - ex.setClaimValue(claimValue) + def ex = new InvalidClaimException(header, claims, claimName, claimValue, msg, cause) verify header, claims diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy index c54b430d..e5d47c70 100644 --- a/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/JwtHandlerAdapterTest.groovy @@ -15,52 +15,78 @@ */ package io.jsonwebtoken +import org.junit.Before import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class JwtHandlerAdapterTest { + private JwtHandlerAdapter handler + + @Before + void setUp() { + handler = new JwtHandlerAdapter(){} + } + @Test - void testOnPlaintextJwt() { - def handler = new JwtHandlerAdapter(); + void testOnContentJwt() { try { - handler.onPlaintextJwt(null) + handler.onContentJwt(null) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @Test void testOnClaimsJwt() { - def handler = new JwtHandlerAdapter(); try { handler.onClaimsJwt(null) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', e.getMessage() } } @Test - void testOnPlaintextJws() { - def handler = new JwtHandlerAdapter(); + void testOnContentJws() { try { - handler.onPlaintextJws(null) + handler.onContentJws(null) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed plaintext JWSs are not supported.' + assertEquals 'Signed content JWTs are not supported.', e.getMessage() } } @Test void testOnClaimsJws() { - def handler = new JwtHandlerAdapter(); try { handler.onClaimsJws(null) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() + } + } + + @Test + void testOnContentJwe() { + try { + handler.onContentJwe(null) + fail() + } catch (UnsupportedJwtException e) { + assertEquals 'Encrypted content JWTs are not supported.', e.getMessage() + } + } + + @Test + void testOnClaimsJwe() { + try { + handler.onClaimsJwe(null) + fail() + } catch (UnsupportedJwtException e) { + assertEquals 'Encrypted Claims JWTs are not supported.', e.getMessage() } } } diff --git a/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy deleted file mode 100644 index f42793b1..00000000 --- a/api/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (C) 2014 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 - -import io.jsonwebtoken.lang.Classes -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import static org.easymock.EasyMock.createMock -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.easymock.EasyMock.same -import static org.junit.Assert.assertSame -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.reset -import static org.powermock.api.easymock.PowerMock.verify - -@RunWith(PowerMockRunner.class) -@PrepareForTest([Classes, Jwts]) -class JwtsTest { - - @Test - void testPrivateCtor() { //for code coverage only - new Jwts() - } - - @Test - void testHeader() { - - mockStatic(Classes) - - def instance = createMock(Header) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultHeader"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.header() - - verify Classes, instance - } - - @Test - void testHeaderFromMap() { - - mockStatic(Classes) - - def map = [:] - - def instance = createMock(Header) - - expect(Classes.newInstance( - eq("io.jsonwebtoken.impl.DefaultHeader"), - same(Jwts.MAP_ARG), - same(map)) - ).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.header(map) - - verify Classes, instance - } - - @Test - void testJwsHeader() { - - mockStatic(Classes) - - def instance = createMock(JwsHeader) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwsHeader"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.jwsHeader() - - verify Classes, instance - } - - @Test - void testJwsHeaderFromMap() { - - mockStatic(Classes) - - def map = [:] - - def instance = createMock(JwsHeader) - - expect(Classes.newInstance( - eq("io.jsonwebtoken.impl.DefaultJwsHeader"), - same(Jwts.MAP_ARG), - same(map)) - ).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.jwsHeader(map) - - verify Classes, instance - } - - @Test - void testClaims() { - - mockStatic(Classes) - - def instance = createMock(Claims) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultClaims"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.claims() - - verify Classes, instance - } - - @Test - void testClaimsFromMap() { - - mockStatic(Classes) - - def map = [:] - - def instance = createMock(Claims) - - expect(Classes.newInstance( - eq("io.jsonwebtoken.impl.DefaultClaims"), - same(Jwts.MAP_ARG), - same(map)) - ).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.claims(map) - - verify Classes, instance - } - - @Test - void testParser() { - - mockStatic(Classes) - - def instance = createMock(JwtParser) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtParser"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.parser() - - verify Classes, instance - } - - @Test - void testBuilder() { - - mockStatic(Classes) - - def instance = createMock(JwtBuilder) - - expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtBuilder"))).andReturn(instance) - - replay Classes, instance - - assertSame instance, Jwts.builder() - - verify Classes, instance - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy index 377eae15..65b1d3fc 100644 --- a/api/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy @@ -34,9 +34,7 @@ class MissingClaimExceptionTest { replay header, claims - def ex = new MissingClaimException(header, claims, msg) - ex.setClaimName(claimName) - ex.setClaimValue(claimValue) + def ex = new MissingClaimException(header, claims, claimName, claimValue, msg) verify header, claims @@ -59,9 +57,7 @@ class MissingClaimExceptionTest { replay header, claims - def ex = new MissingClaimException(header, claims, msg, cause) - ex.setClaimName(claimName) - ex.setClaimValue(claimValue) + def ex = new MissingClaimException(header, claims, claimName, claimValue, msg, cause) verify header, claims diff --git a/api/src/test/groovy/io/jsonwebtoken/SigningKeyResolverAdapterTest.groovy b/api/src/test/groovy/io/jsonwebtoken/SigningKeyResolverAdapterTest.groovy index d3520802..809c5968 100644 --- a/api/src/test/groovy/io/jsonwebtoken/SigningKeyResolverAdapterTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/SigningKeyResolverAdapterTest.groovy @@ -18,6 +18,7 @@ package io.jsonwebtoken import org.junit.Test import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets import static org.easymock.EasyMock.* import static org.junit.Assert.* @@ -34,7 +35,7 @@ class SigningKeyResolverAdapterTest { @Test(expected=UnsupportedJwtException) //should throw since called but not overridden void testDefaultResolveSigningKeyBytesFromStringPayload() { def header = createMock(JwsHeader) - new SigningKeyResolverAdapter().resolveSigningKeyBytes(header, "hi") + new SigningKeyResolverAdapter().resolveSigningKeyBytes(header, "hi".getBytes(StandardCharsets.UTF_8)) } @Test @@ -82,8 +83,9 @@ class SigningKeyResolverAdapterTest { JwsHeader header = createMock(JwsHeader) - byte[] bytes = new byte[32] - new Random().nextBytes(bytes) + byte[] keyBytes = new byte[32] + new Random().nextBytes(keyBytes) + byte[] payloadBytes = 'hi'.getBytes(StandardCharsets.UTF_8) expect(header.getAlgorithm()).andReturn("HS256") @@ -91,20 +93,20 @@ class SigningKeyResolverAdapterTest { def adapter = new SigningKeyResolverAdapter() { @Override - byte[] resolveSigningKeyBytes(JwsHeader h, String s) { + byte[] resolveSigningKeyBytes(JwsHeader h, byte[] payload) { assertSame header, h - assertEquals 'hi', s - return bytes + assertArrayEquals payloadBytes, payload + return keyBytes } } - def key = adapter.resolveSigningKey(header, 'hi') + def key = adapter.resolveSigningKey(header, payloadBytes) verify header assertTrue key instanceof SecretKeySpec assertEquals 'HmacSHA256', key.algorithm - assertTrue Arrays.equals(bytes, key.encoded) + assertTrue Arrays.equals(keyBytes, key.encoded) } @Test(expected=IllegalArgumentException) @@ -112,6 +114,6 @@ class SigningKeyResolverAdapterTest { JwsHeader header = createMock(JwsHeader) expect(header.getAlgorithm()).andReturn("RS256") replay header - new SigningKeyResolverAdapter().resolveSigningKey(header, 'hi') + new SigningKeyResolverAdapter().resolveSigningKey(header, 'hi'.getBytes(StandardCharsets.UTF_8)) } } diff --git a/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy index a10acc74..1a8ad1a6 100644 --- a/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/io/DecodersTest.groovy @@ -17,13 +17,17 @@ package io.jsonwebtoken.io import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertTrue class DecodersTest { @Test - void testBase64() { + void testPrivateCtor() { new Decoders() //not allowed in java, including here only to pass test coverage assertions + } + + @Test + void testBase64() { assertTrue Decoders.BASE64 instanceof ExceptionPropagatingDecoder assertTrue Decoders.BASE64.decoder instanceof Base64Decoder } diff --git a/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy index d4b323cb..57d25c72 100644 --- a/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/io/EncodersTest.groovy @@ -17,13 +17,17 @@ package io.jsonwebtoken.io import org.junit.Test -import static org.junit.Assert.* +import static org.junit.Assert.assertTrue class EncodersTest { @Test - void testBase64() { + void testPrivateCtor() { new Encoders() //not allowed in java, including here only to pass test coverage assertions + } + + @Test + void testBase64() { assertTrue Encoders.BASE64 instanceof ExceptionPropagatingEncoder assertTrue Encoders.BASE64.encoder instanceof Base64Encoder } diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy new file mode 100644 index 00000000..48c40ef5 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/lang/ArraysTest.groovy @@ -0,0 +1,66 @@ +/* + * Copyright © 2018 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.lang + + +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class ArraysTest { + + @Test + void testPrivateCtor() { + new Arrays() //not allowed in java, including here only to pass test coverage assertions + } + + @Test + void testCleanWithNull() { + assertNull Arrays.clean(null) + } + + @Test + void testCleanWithEmpty() { + assertNull Arrays.clean(new byte[0]) + } + + @Test + void testCleanWithElements() { + byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8) + assertSame bytes, Arrays.clean(bytes) + } + + @Test + void testByteArrayLengthWithNull() { + assertEquals 0, Arrays.length((byte[]) null) + } + + @Test + void testByteArrayLengthWithEmpty() { + assertEquals 0, Arrays.length(new byte[0]) + } + + @Test + void testByteArrayLengthWithElements() { + byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8) + assertEquals 5, Arrays.length(bytes) + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy index 36540f43..5afe6de3 100644 --- a/api/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy @@ -26,10 +26,10 @@ class StringsTest { assertFalse Strings.hasText(null) assertFalse Strings.hasText("") assertFalse Strings.hasText(" ") - assertTrue Strings.hasText(" foo "); + assertTrue Strings.hasText(" foo ") assertTrue Strings.hasText("foo") } - + @Test void testClean() { assertEquals "this is a test", Strings.clean("this is a test") @@ -41,34 +41,55 @@ class StringsTest { assertNull Strings.clean("\t") assertNull Strings.clean(" ") } - + @Test void testCleanCharSequence() { - def result = Strings.clean(new StringBuilder("this is a test")) - assertNotNull result + def result = Strings.clean(new StringBuilder("this is a test")) + assertNotNull result assertEquals "this is a test", result.toString() - + result = Strings.clean(new StringBuilder(" this is a test")) - assertNotNull result + assertNotNull result assertEquals "this is a test", result.toString() - + result = Strings.clean(new StringBuilder(" this is a test ")) - assertNotNull result + assertNotNull result assertEquals "this is a test", result.toString() - + result = Strings.clean(new StringBuilder("\nthis is a test \t ")) - assertNotNull result + assertNotNull result assertEquals "this is a test", result.toString() - + assertNull Strings.clean((StringBuilder) null) assertNull Strings.clean(new StringBuilder("")) assertNull Strings.clean(new StringBuilder("\t")) assertNull Strings.clean(new StringBuilder(" ")) } - - + + @Test void testTrimWhitespace() { - assertEquals "", Strings.trimWhitespace(" ") + assertEquals "", Strings.trimWhitespace(" ") + } + + @Test + void testNespaceNull() { + assertNull Strings.nespace(null) + } + + @Test + void testNespaceEmpty() { + StringBuilder sb = new StringBuilder() + Strings.nespace(sb) + assertEquals 0, sb.length() // didn't add space because it's already empty + assertEquals '', sb.toString() + } + + @Test + void testNespaceNonEmpty() { + StringBuilder sb = new StringBuilder() + sb.append("Hello") + Strings.nespace(sb).append("World") + assertEquals 'Hello World', sb.toString() } } diff --git a/api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy new file mode 100644 index 00000000..c9f15d40 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/InvalidKeyExceptionTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright © 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.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class InvalidKeyExceptionTest { + + @Test + void testDefaultConstructor() { + def msg = "my message" + def exception = new InvalidKeyException(msg) + assertEquals msg, exception.getMessage() + } + + @Test + void testConstructorWithCause() { + def rootMsg = 'root error' + def msg = 'wrapping' + def ioException = new IOException(rootMsg) + def exception = new InvalidKeyException(msg, ioException) + assertEquals msg, exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy deleted file mode 100644 index 7635491c..00000000 --- a/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2014 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.security - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.lang.Classes -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import javax.crypto.SecretKey -import java.security.KeyPair - -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.easymock.EasyMock.same -import static org.junit.Assert.* -import static org.powermock.api.easymock.PowerMock.* - -/** - * This test class is for cursory API-level testing only (what is available to the API module at build time). - * - * The actual implementation assertions are done in KeysImplTest in the impl module. - */ -@RunWith(PowerMockRunner) -@PrepareForTest([Classes, Keys]) -class KeysTest { - - @Test - void testPrivateCtor() { //for code coverage only - new Keys() - } - - @Test - void testHmacShaKeyForWithNullArgument() { - try { - Keys.hmacShaKeyFor(null) - } catch (InvalidKeyException expected) { - assertEquals 'SecretKey byte array cannot be null.', expected.message - } - } - - @Test - void testHmacShaKeyForWithWeakKey() { - int numBytes = 31 - int numBits = numBytes * 8 - try { - Keys.hmacShaKeyFor(new byte[numBytes]) - } catch (WeakKeyException expected) { - assertEquals "The specified key byte array is " + numBits + " bits which " + - "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + - "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + - "size >= 256 bits (the key size must be greater than or equal to the hash " + - "output size). Consider using the " + Keys.class.getName() + "#secretKeyFor(SignatureAlgorithm) method " + - "to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See " + - "https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message - } - } - - @Test - void testSecretKeyFor() { - - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { - - String name = alg.name() - - if (name.startsWith('H')) { - - mockStatic(Classes) - - def key = createMock(SecretKey) - expect(Classes.invokeStatic(eq(Keys.MAC), eq("generateKey"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(key) - - replay Classes, key - - assertSame key, Keys.secretKeyFor(alg) - - verify Classes, key - - reset Classes, key - - } else { - try { - Keys.secretKeyFor(alg) - fail() - } catch (IllegalArgumentException expected) { - assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message - } - - } - } - - } - - @Test - void testKeyPairFor() { - - for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { - - String name = alg.name() - - if (name.equals('NONE') || name.startsWith('H')) { - try { - Keys.keyPairFor(alg) - fail() - } catch (IllegalArgumentException expected) { - assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message - } - } else { - String fqcn = name.startsWith('E') ? Keys.EC : Keys.RSA - - mockStatic Classes - - def pair = createMock(KeyPair) - expect(Classes.invokeStatic(eq(fqcn), eq("generateKeyPair"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(pair) - - replay Classes, pair - - assertSame pair, Keys.keyPairFor(alg) - - verify Classes, pair - - reset Classes, pair - } - } - } -} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy new file mode 100644 index 00000000..5eb9a55a --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/MalformedKeyExceptionTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright © 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.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class MalformedKeyExceptionTest { + + @Test + void testDefaultConstructor() { + def msg = "my message" + def exception = new MalformedKeyException(msg) + assertEquals msg, exception.getMessage() + } + + @Test + void testConstructorWithCause() { + def rootMsg = 'root error' + def msg = 'wrapping' + def ioException = new IOException(rootMsg) + def exception = new MalformedKeyException(msg, ioException) + assertEquals msg, exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy new file mode 100644 index 00000000..40529944 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/UnsupportedKeyExceptionTest.groovy @@ -0,0 +1,33 @@ +/* + * Copyright © 2018 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.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class UnsupportedKeyExceptionTest { + + @Test + void testCauseWithMessage() { + def cause = new IllegalStateException() + def msg = 'foo' + def ex = new UnsupportedKeyException(msg, cause) + assertEquals msg, ex.getMessage() + assertSame cause, ex.getCause() + } +} diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml index 3c03268b..fff8e154 100644 --- a/extensions/gson/pom.xml +++ b/extensions/gson/pom.xml @@ -44,4 +44,13 @@ + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + + \ No newline at end of file diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java index bb0d76f5..01099a50 100644 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSerializer.java @@ -22,11 +22,14 @@ import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.Supplier; public class GsonSerializer implements Serializer { - static final Gson DEFAULT_GSON = new GsonBuilder().disableHtmlEscaping().create(); - private Gson gson; + static final Gson DEFAULT_GSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create(); + private final Gson gson; @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator public GsonSerializer() { @@ -37,6 +40,17 @@ public class GsonSerializer implements Serializer { public GsonSerializer(Gson gson) { Assert.notNull(gson, "gson cannot be null."); this.gson = gson; + + //ensure the necessary type adapter has been registered, and if not, throw an error: + String json = this.gson.toJson(TestSupplier.INSTANCE); + if (json.contains("value")) { + String msg = "Invalid Gson instance - it has not been registered with the necessary " + + Supplier.class.getName() + " type adapter. When using the GsonBuilder, ensure this " + + "type adapter is registered by calling gsonBuilder.registerTypeHierarchyAdapter(" + + Supplier.class.getName() + ".class, " + + GsonSupplierSerializer.class.getName() + ".INSTANCE) before calling gsonBuilder.create()"; + throw new IllegalArgumentException(msg); + } } @Override @@ -62,4 +76,19 @@ public class GsonSerializer implements Serializer { } return this.gson.toJson(o).getBytes(Strings.UTF_8); } + + private static class TestSupplier implements Supplier { + + private static final TestSupplier INSTANCE = new TestSupplier<>("test"); + private final T value; + + private TestSupplier(T value) { + this.value = value; + } + + @Override + public T get() { + return value; + } + } } diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java new file mode 100644 index 00000000..8ae51171 --- /dev/null +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonSupplierSerializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.gson.io; + +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.jsonwebtoken.lang.Supplier; + +import java.lang.reflect.Type; + +public final class GsonSupplierSerializer implements JsonSerializer> { + + public static final GsonSupplierSerializer INSTANCE = new GsonSupplierSerializer(); + + @Override + public JsonElement serialize(Supplier supplier, Type type, JsonSerializationContext ctx) { + Object value = supplier.get(); + return ctx.serialize(value); + } +} diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy index ae122396..581de42c 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy @@ -21,6 +21,9 @@ import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.lang.Strings import org.junit.Test +import java.text.DecimalFormat +import java.text.NumberFormat + import static org.easymock.EasyMock.* import static org.junit.Assert.* import static org.hamcrest.CoreMatchers.instanceOf diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy index be2abe48..4fd6dfb9 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonSerializerTest.groovy @@ -15,23 +15,23 @@ */ package io.jsonwebtoken.gson.io -import io.jsonwebtoken.io.Deserializer +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.lang.Supplier import org.junit.Test import static org.easymock.EasyMock.* import static org.junit.Assert.* -import static org.hamcrest.CoreMatchers.instanceOf -import com.google.gson.Gson -import io.jsonwebtoken.io.SerializationException class GsonSerializerTest { @Test void loadService() { def serializer = ServiceLoader.load(Serializer).iterator().next() - assertThat(serializer, instanceOf(GsonSerializer)) + assertTrue serializer instanceof GsonSerializer } @Test @@ -41,14 +41,32 @@ class GsonSerializerTest { } @Test - void testObjectMapperConstructor() { - def customGSON = new Gson() + void testGsonConstructor() { + def customGSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE) + .disableHtmlEscaping().create() def serializer = new GsonSerializer<>(customGSON) assertSame customGSON, serializer.gson } + @Test + void testGsonConstructorWithIncorrectlyConfiguredGson() { + try { + //noinspection GroovyResultOfObjectAllocationIgnored + new GsonSerializer<>(new Gson()) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'Invalid Gson instance - it has not been registered with the necessary ' + + 'io.jsonwebtoken.lang.Supplier type adapter. When using the GsonBuilder, ensure this type ' + + 'adapter is registered by calling ' + + 'gsonBuilder.registerTypeHierarchyAdapter(io.jsonwebtoken.lang.Supplier.class, ' + + 'io.jsonwebtoken.gson.io.GsonSupplierSerializer.INSTANCE) before calling gsonBuilder.create()' + assertEquals msg, expected.message + } + } + @Test(expected = IllegalArgumentException) - void testObjectMapperConstructorWithNullArgument() { + void testConstructorWithNullArgument() { new GsonSerializer<>(null) } @@ -94,7 +112,7 @@ class GsonSerializerTest { assertTrue Arrays.equals(expected, result) } - + @Test void testSerializeFailsWithJsonProcessingException() { diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 2ff09ca3..6567ebcd 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -66,14 +66,6 @@ com.github.siom79.japicmp japicmp-maven-plugin - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-deprecated.${project.packaging} - - - diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index a9ea111e..7aa5acf8 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -18,7 +18,6 @@ package io.jsonwebtoken.jackson.io; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; - import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import io.jsonwebtoken.io.DeserializationException; @@ -37,7 +36,6 @@ public class JacksonDeserializer implements Deserializer { private final Class returnType; private final ObjectMapper objectMapper; - @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator public JacksonDeserializer() { this(JacksonSerializer.DEFAULT_OBJECT_MAPPER); } @@ -64,10 +62,10 @@ public class JacksonDeserializer implements Deserializer { * If you would like to use your own {@code ObjectMapper} instance that also supports custom types for * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering * your custom types and then use the {@link #JacksonDeserializer(ObjectMapper)} constructor instead. - * + * * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type */ - public JacksonDeserializer(Map claimTypeMap) { + public JacksonDeserializer(Map> claimTypeMap) { // DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer // between instances this(new ObjectMapper()); @@ -110,9 +108,9 @@ public class JacksonDeserializer implements Deserializer { */ private static class MappedTypeDeserializer extends UntypedObjectDeserializer { - private final Map claimTypeMap; + private final Map> claimTypeMap; - private MappedTypeDeserializer(Map claimTypeMap) { + private MappedTypeDeserializer(Map> claimTypeMap) { super(null, null); this.claimTypeMap = claimTypeMap; } @@ -122,7 +120,7 @@ public class JacksonDeserializer implements Deserializer { // check if the current claim key is mapped, if so traverse it's value String name = parser.currentName(); if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) { - Class type = claimTypeMap.get(name); + Class type = claimTypeMap.get(name); return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type); } // otherwise default to super diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java index 1445f92d..da05c5ae 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java @@ -16,7 +16,9 @@ package io.jsonwebtoken.jackson.io; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; @@ -26,7 +28,16 @@ import io.jsonwebtoken.lang.Assert; */ public class JacksonSerializer implements Serializer { - static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper(); + static final String MODULE_ID = "jjwt-jackson"; + static final Module MODULE; + + static { + SimpleModule module = new SimpleModule(MODULE_ID); + module.addSerializer(JacksonSupplierSerializer.INSTANCE); + MODULE = module; + } + + static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); private final ObjectMapper objectMapper; @@ -38,7 +49,7 @@ public class JacksonSerializer implements Serializer { @SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper public JacksonSerializer(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper cannot be null."); - this.objectMapper = objectMapper; + this.objectMapper = objectMapper.registerModule(MODULE); } @Override diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java new file mode 100644 index 00000000..a415bcf0 --- /dev/null +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSupplierSerializer.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 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 com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import io.jsonwebtoken.lang.Supplier; + +import java.io.IOException; + +final class JacksonSupplierSerializer extends StdSerializer> { + + static final JacksonSupplierSerializer INSTANCE = new JacksonSupplierSerializer(); + + public JacksonSupplierSerializer() { + super(Supplier.class, false); + } + + @Override + public void serialize(Supplier supplier, JsonGenerator generator, SerializerProvider provider) throws IOException { + Object value = supplier.get(); + + if (value == null) { + provider.defaultSerializeNull(generator); + return; + } + + Class clazz = value.getClass(); + JsonSerializer ser = provider.findTypedValueSerializer(clazz, true, null); + ser.serialize(value, generator, provider); + } +} diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy index 1febe76e..6112ad27 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSerializerTest.groovy @@ -23,8 +23,8 @@ import io.jsonwebtoken.lang.Strings import org.junit.Test import static org.easymock.EasyMock.* -import static org.junit.Assert.* import static org.hamcrest.CoreMatchers.instanceOf +import static org.junit.Assert.* class JacksonSerializerTest { @@ -52,6 +52,15 @@ class JacksonSerializerTest { new JacksonSerializer<>(null) } + @Test + void testObjectMapperConstructorAutoRegistersModule() { + def om = createMock(ObjectMapper) + expect(om.registerModule(same(JacksonSerializer.MODULE))).andReturn(om) + replay om + def serializer = new JacksonSerializer<>(om) + verify om + } + @Test void testByte() { byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120 diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy new file mode 100644 index 00000000..9c6cb03d --- /dev/null +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonSupplierSerializerTest.groovy @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 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 +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertEquals + +class JacksonSupplierSerializerTest { + + @Test + void testSupplierNullValue() { + def serializer = new JacksonSerializer() + def supplier = new Supplier() { + @Override + Object get() { + return null + } + } + byte[] bytes = serializer.serialize(supplier) + assertEquals 'null', new String(bytes, StandardCharsets.UTF_8) + } +} diff --git a/extensions/orgjson/pom.xml b/extensions/orgjson/pom.xml index 7a434767..de750bc2 100644 --- a/extensions/orgjson/pom.xml +++ b/extensions/orgjson/pom.xml @@ -66,14 +66,6 @@ com.github.siom79.japicmp japicmp-maven-plugin - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-deprecated.${project.packaging} - - - diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java index 2b5c2b76..e1b31f01 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java @@ -35,7 +35,6 @@ import java.util.Map; */ public class OrgJsonDeserializer implements Deserializer { - @SuppressWarnings("unchecked") @Override public Object deserialize(byte[] bytes) throws DeserializationException { @@ -91,7 +90,7 @@ public class OrgJsonDeserializer implements Deserializer { int length = a.length(); List list = new ArrayList<>(length); // https://github.com/jwtk/jjwt/issues/380: use a.get(i) and *not* a.toList() for Android compatibility: - for( int i = 0; i < length; i++) { + for (int i = 0; i < length; i++) { Object value = a.get(i); value = convertIfNecessary(value); list.add(value); diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java index b6ea242c..c46ee250 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java @@ -23,6 +23,7 @@ import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.DateFormats; import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.Supplier; import org.json.JSONArray; import org.json.JSONObject; @@ -40,9 +41,9 @@ public class OrgJsonSerializer implements Serializer { // 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 Class[] VALUE_TO_STRING_ARG_TYPES = new Class[]{Object.class}; + private static final Class[] VALUE_TO_STRING_ARG_TYPES = new Class[]{Object.class}; private static final String JSON_STRING_CLASS_NAME = "org.json.JSONString"; - private static final Class JSON_STRING_CLASS; + private static final Class JSON_STRING_CLASS; static { // see see https://github.com/jwtk/jjwt/issues/388 if (Classes.isAvailable(JSON_STRING_CLASS_NAME)) { @@ -82,14 +83,18 @@ public class OrgJsonSerializer implements Serializer { return JSONObject.NULL; } + if (object instanceof Supplier) { + object = ((Supplier)object).get(); + } + if (object instanceof JSONObject || object instanceof JSONArray - || JSONObject.NULL.equals(object) || isJSONString(object) - || object instanceof Byte || object instanceof Character - || object instanceof Short || object instanceof Integer - || object instanceof Long || object instanceof Boolean - || object instanceof Float || object instanceof Double - || object instanceof String || object instanceof BigInteger - || object instanceof BigDecimal || object instanceof Enum) { + || JSONObject.NULL.equals(object) || isJSONString(object) + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { return object; } @@ -114,14 +119,15 @@ public class OrgJsonSerializer implements Serializer { Map map = (Map) object; return toJSONObject(map); } + + if (Objects.isArray(object)) { + object = Collections.arrayToList(object); //sets object to List, will be converted in next if-statement: + } + if (object instanceof Collection) { Collection coll = (Collection) object; return toJSONArray(coll); } - if (Objects.isArray(object)) { - Collection c = Collections.arrayToList(object); - return toJSONArray(c); - } //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: @@ -145,7 +151,7 @@ public class OrgJsonSerializer implements Serializer { return obj; } - private JSONArray toJSONArray(Collection c) { + private JSONArray toJSONArray(Collection c) { JSONArray array = new JSONArray(); @@ -167,7 +173,7 @@ public class OrgJsonSerializer implements Serializer { // // This is sufficient for all JJWT-supported scenarios on Android since Android users shouldn't ever 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 outside of that. + // is within the context of JwtBuilder execution and not for application use beyond that. if (o instanceof JSONObject) { s = o.toString(); } else { diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy index 2cca2efb..d47af232 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/AndroidOrgJsonSerializerTest.groovy @@ -16,7 +16,6 @@ package io.jsonwebtoken.orgjson.io import io.jsonwebtoken.lang.Classes -import io.jsonwebtoken.orgjson.io.OrgJsonSerializer import org.junit.Test import org.junit.runner.RunWith import org.powermock.core.classloader.annotations.PrepareForTest @@ -24,10 +23,8 @@ import org.powermock.modules.junit4.PowerMockRunner import static org.easymock.EasyMock.eq import static org.easymock.EasyMock.expect -import static org.junit.Assert.* -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.verify +import static org.junit.Assert.assertFalse +import static org.powermock.api.easymock.PowerMock.* @RunWith(PowerMockRunner.class) @PrepareForTest([Classes]) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractHeader.java new file mode 100644 index 00000000..8e265df4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractHeader.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 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.Header; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; + +import java.util.Map; +import java.util.Set; + +public abstract class AbstractHeader> extends JwtMap implements Header { + + static final Field TYPE = Fields.string(Header.TYPE, "Type"); + static final Field CONTENT_TYPE = Fields.string(Header.CONTENT_TYPE, "Content Type"); + static final Field ALGORITHM = Fields.string(Header.ALGORITHM, "Algorithm"); + static final Field COMPRESSION_ALGORITHM = Fields.string(Header.COMPRESSION_ALGORITHM, "Compression Algorithm"); + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated // TODO: remove for 1.0.0: + static final Field DEPRECATED_COMPRESSION_ALGORITHM = Fields.string(Header.DEPRECATED_COMPRESSION_ALGORITHM, "Deprecated Compression Algorithm"); + + static final Set> FIELDS = Collections.>setOf(TYPE, CONTENT_TYPE, ALGORITHM, COMPRESSION_ALGORITHM, DEPRECATED_COMPRESSION_ALGORITHM); + + protected AbstractHeader(Set> fieldSet) { + super(fieldSet); + } + + protected AbstractHeader(Set> fieldSet, Map values) { + super(fieldSet, values); + } + + @Override + public String getName() { + return "JWT header"; + } + + @SuppressWarnings("unchecked") + protected T tthis() { + return (T) this; + } + + @Override + public String getType() { + return idiomaticGet(TYPE); + } + + @Override + public T setType(String typ) { + put(TYPE, typ); + return tthis(); + } + + @Override + public String getContentType() { + return idiomaticGet(CONTENT_TYPE); + } + + @Override + public T setContentType(String cty) { + put(CONTENT_TYPE, cty); + return tthis(); + } + + @Override + public String getAlgorithm() { + return idiomaticGet(ALGORITHM); + } + + @Override + public T setAlgorithm(String alg) { + put(ALGORITHM, alg); + return tthis(); + } + + @Override + public String getCompressionAlgorithm() { + String s = idiomaticGet(COMPRESSION_ALGORITHM); + if (!Strings.hasText(s)) { + s = idiomaticGet(DEPRECATED_COMPRESSION_ALGORITHM); + } + return s; + } + + @Override + public T setCompressionAlgorithm(String compressionAlgorithm) { + put(COMPRESSION_ALGORITHM, compressionAlgorithm); + return tthis(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractHeaderBuilder.java new file mode 100644 index 00000000..4afb4523 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractHeaderBuilder.java @@ -0,0 +1,97 @@ +/* + * 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.Header; + +import java.util.Map; + +/** + * @since JJWT_RELEASE_VERSION + */ +public abstract class AbstractHeaderBuilder, T extends HeaderBuilder> + implements HeaderBuilder { + + protected final H header; + + protected AbstractHeaderBuilder() { + this.header = newHeader(); + onNewHeader(this.header); + } + + protected abstract H newHeader(); + + protected void onNewHeader(H header) { + } + + @SuppressWarnings("unchecked") + protected final T tthis() { + return (T) this; + } + + @Override + public T setType(String typ) { + this.header.setType(typ); + return tthis(); + } + + @Override + public T setContentType(String cty) { + this.header.setContentType(cty); + return tthis(); + } + + @Override + public T setAlgorithm(String alg) { + this.header.setAlgorithm(alg); + return tthis(); + } + + @Override + public T setCompressionAlgorithm(String zip) { + this.header.setCompressionAlgorithm(zip); + return tthis(); + } + + @Override + public H build() { + return this.header; + } + + @Override + public T put(String key, Object value) { + this.header.put(key, value); + return tthis(); + } + + @Override + public T remove(String key) { + this.header.remove(key); + return tthis(); + } + + @Override + public T putAll(Map m) { + this.header.putAll(m); + return tthis(); + } + + @Override + public T clear() { + this.header.clear(); + return tthis(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java new file mode 100644 index 00000000..057f0c7f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeader.java @@ -0,0 +1,132 @@ +/* + * 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.ProtectedHeader; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.security.AbstractAsymmetricJwk; +import io.jsonwebtoken.impl.security.AbstractJwk; +import io.jsonwebtoken.impl.security.JwkConverter; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Header implementation satisfying shared JWS and JWE header parameter requirements. Header parameters specific to + * either JWE or JWS will be defined in respective subclasses. + * + * @param specific header type to return from mutation/setter methods for method chaining + * @since JJWT_RELEASE_VERSION + */ +public abstract class AbstractProtectedHeader> extends AbstractHeader implements ProtectedHeader { + + static final Field JKU = Fields.uri("jku", "JWK Set URL"); + + @SuppressWarnings("unchecked") + static final Field> JWK = Fields.builder((Class>) (Class) PublicJwk.class) + .setId("jwk").setName("JSON Web Key") + .setConverter(JwkConverter.PUBLIC_JWK).build(); + static final Field> CRIT = Fields.stringSet("crit", "Critical"); + + static final Set> FIELDS = Collections.concat(AbstractHeader.FIELDS, CRIT, JKU, JWK, AbstractJwk.KID, + AbstractAsymmetricJwk.X5U, AbstractAsymmetricJwk.X5C, AbstractAsymmetricJwk.X5T, AbstractAsymmetricJwk.X5T_S256); + + protected AbstractProtectedHeader(Set> fieldSet) { + super(fieldSet); + } + + protected AbstractProtectedHeader(Set> fieldSet, Map values) { + super(fieldSet, values); + } + + public String getKeyId() { + return idiomaticGet(AbstractJwk.KID); + } + + public T setKeyId(String kid) { + put(AbstractJwk.KID, kid); + return tthis(); + } + + public URI getJwkSetUrl() { + return idiomaticGet(JKU); + } + + public T setJwkSetUrl(URI uri) { + put(JKU, uri); + return tthis(); + } + + public PublicJwk getJwk() { + return idiomaticGet(JWK); + } + + public T setJwk(PublicJwk jwk) { + put(JWK, jwk); + return tthis(); + } + + public URI getX509Url() { + return idiomaticGet(AbstractAsymmetricJwk.X5U); + } + + public T setX509Url(URI uri) { + put(AbstractAsymmetricJwk.X5U, uri); + return tthis(); + } + + public List getX509CertificateChain() { + return idiomaticGet(AbstractAsymmetricJwk.X5C); + } + + public T setX509CertificateChain(List chain) { + put(AbstractAsymmetricJwk.X5C, chain); + return tthis(); + } + + public byte[] getX509CertificateSha1Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T); + } + + public T setX509CertificateSha1Thumbprint(byte[] thumbprint) { + put(AbstractAsymmetricJwk.X5T, thumbprint); + return tthis(); + } + + public byte[] getX509CertificateSha256Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T_S256); + } + + public T setX509CertificateSha256Thumbprint(byte[] thumbprint) { + put(AbstractAsymmetricJwk.X5T_S256, thumbprint); + return tthis(); + } + + public Set getCritical() { + return idiomaticGet(CRIT); + } + + public T setCritical(Set crit) { + put(CRIT, crit); + return tthis(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeaderBuilder.java new file mode 100644 index 00000000..453b6f59 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/AbstractProtectedHeaderBuilder.java @@ -0,0 +1,102 @@ +/* + * 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.ProtectedHeader; +import io.jsonwebtoken.impl.security.DefaultX509Builder; +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public abstract class AbstractProtectedHeaderBuilder, + T extends ProtectedHeaderBuilder> + extends AbstractHeaderBuilder implements ProtectedHeaderBuilder { + + private DefaultX509Builder x509Builder; + + @Override + protected void onNewHeader(H header) { + this.x509Builder = new DefaultX509Builder<>(header, tthis(), IllegalStateException.class); + } + + @Override + public T setJwkSetUrl(URI uri) { + this.header.setJwkSetUrl(uri); + return tthis(); + } + + @Override + public T setJwk(PublicJwk jwk) { + this.header.setJwk(jwk); + return tthis(); + } + + @Override + public T setKeyId(String kid) { + this.header.setKeyId(kid); + return tthis(); + } + + @Override + public T setX509Url(URI uri) { + return this.x509Builder.setX509Url(uri); + } + + @Override + public T setX509CertificateChain(List chain) { + return this.x509Builder.setX509CertificateChain(chain); + } + + @Override + public T setX509CertificateSha1Thumbprint(byte[] thumbprint) { + this.header.setX509CertificateSha1Thumbprint(thumbprint); + return tthis(); + } + + @Override + public T setX509CertificateSha256Thumbprint(byte[] thumbprint) { + this.header.setX509CertificateSha256Thumbprint(thumbprint); + return tthis(); + } + + @Override + public T setCritical(Set crit) { + this.header.setCritical(crit); + return tthis(); + } + + @Override + public T withX509Sha1Thumbprint(boolean enable) { + return x509Builder.withX509Sha1Thumbprint(enable); + } + + @Override + public T withX509Sha256Thumbprint(boolean enable) { + return x509Builder.withX509Sha256Thumbprint(enable); + } + + @Override + public H build() { + this.x509Builder.apply(); + return this.header; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java new file mode 100644 index 00000000..bdfa04cc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/CompressionCodecLocator.java @@ -0,0 +1,42 @@ +/* + * 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.CompressionCodec; +import io.jsonwebtoken.CompressionCodecResolver; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.lang.Assert; + +public class CompressionCodecLocator implements Function, CompressionCodec>, Locator { + + private final CompressionCodecResolver resolver; + + public CompressionCodecLocator(CompressionCodecResolver resolver) { + this.resolver = Assert.notNull(resolver, "CompressionCodecResolver cannot be null."); + } + + @Override + public CompressionCodec apply(Header header) { + return resolver.resolveCompressionCodec(header); + } + + @Override + public CompressionCodec locate(Header header) { + return apply(header); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 32194da4..c0379cae 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -17,8 +17,15 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Claims; import io.jsonwebtoken.RequiredTypeException; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.JwtDateConverter; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; + import java.util.Date; import java.util.Map; +import java.util.Set; public class DefaultClaims extends JwtMap implements Claims { @@ -30,141 +37,156 @@ public class DefaultClaims extends JwtMap implements Claims { "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; + static final Field ISSUER = Fields.string(Claims.ISSUER, "Issuer"); + static final Field SUBJECT = Fields.string(Claims.SUBJECT, "Subject"); + static final Field AUDIENCE = Fields.string(Claims.AUDIENCE, "Audience"); + static final Field EXPIRATION = Fields.rfcDate(Claims.EXPIRATION, "Expiration Time"); + static final Field NOT_BEFORE = Fields.rfcDate(Claims.NOT_BEFORE, "Not Before"); + static final Field ISSUED_AT = Fields.rfcDate(Claims.ISSUED_AT, "Issued At"); + static final Field JTI = Fields.string(Claims.ID, "JWT ID"); + + static final Set> FIELDS = Collections.>setOf( + ISSUER, SUBJECT, AUDIENCE, EXPIRATION, NOT_BEFORE, ISSUED_AT, JTI + ); + public DefaultClaims() { - super(); + super(FIELDS); } public DefaultClaims(Map map) { - super(map); + super(FIELDS, map); + } + + @Override + public String getName() { + return "JWT Claim"; } @Override public String getIssuer() { - return getString(ISSUER); + return idiomaticGet(ISSUER); } @Override public Claims setIssuer(String iss) { - setValue(ISSUER, iss); + put(ISSUER, iss); return this; } @Override public String getSubject() { - return getString(SUBJECT); + return idiomaticGet(SUBJECT); } @Override public Claims setSubject(String sub) { - setValue(SUBJECT, sub); + put(SUBJECT, sub); return this; } @Override public String getAudience() { - return getString(AUDIENCE); + return idiomaticGet(AUDIENCE); } @Override public Claims setAudience(String aud) { - setValue(AUDIENCE, aud); + put(AUDIENCE, aud); return this; } @Override public Date getExpiration() { - return get(Claims.EXPIRATION, Date.class); + return idiomaticGet(EXPIRATION); } @Override public Claims setExpiration(Date exp) { - setDateAsSeconds(Claims.EXPIRATION, exp); + put(EXPIRATION, exp); return this; } @Override public Date getNotBefore() { - return get(Claims.NOT_BEFORE, Date.class); + return idiomaticGet(NOT_BEFORE); } @Override public Claims setNotBefore(Date nbf) { - setDateAsSeconds(Claims.NOT_BEFORE, nbf); + put(NOT_BEFORE, nbf); return this; } @Override public Date getIssuedAt() { - return get(Claims.ISSUED_AT, Date.class); + return idiomaticGet(ISSUED_AT); } @Override public Claims setIssuedAt(Date iat) { - setDateAsSeconds(Claims.ISSUED_AT, iat); + put(ISSUED_AT, iat); return this; } @Override public String getId() { - return getString(ID); + return idiomaticGet(JTI); } @Override public Claims setId(String jti) { - setValue(Claims.ID, jti); + put(JTI, jti); return this; } - /** - * @since 0.10.0 - */ - private static boolean isSpecDate(String claimName) { - return Claims.EXPIRATION.equals(claimName) || - Claims.ISSUED_AT.equals(claimName) || - Claims.NOT_BEFORE.equals(claimName); - } - - @Override - public Object put(String s, Object o) { - if (o instanceof Date && isSpecDate(s)) { //since 0.10.0 - Date date = (Date)o; - return setDateAsSeconds(s, date); - } - return super.put(s, o); - } - @Override public T get(String claimName, Class requiredType) { + Assert.notNull(requiredType, "requiredType argument cannot be null."); - Object value = get(claimName); + Object value = idiomaticGet(claimName); + if (requiredType.isInstance(value)) { + return requiredType.cast(value); + } + + value = get(claimName); if (value == null) { return null; } if (Date.class.equals(requiredType)) { - if (isSpecDate(claimName)) { - value = toSpecDate(value, claimName); - } else { - value = toDate(value, claimName); + try { + value = JwtDateConverter.toDate(value); // NOT specDate logic + } catch (Exception e) { + String msg = "Cannot create Date from '" + claimName + "' value '" + value + "'. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); } } - return castClaimValue(value, requiredType); + return castClaimValue(claimName, value, requiredType); } - private T castClaimValue(Object value, Class requiredType) { + private T castClaimValue(String name, Object value, Class requiredType) { - if (value instanceof Integer) { - int intValue = (Integer) value; - if (requiredType == Long.class) { - value = (long) intValue; - } else if (requiredType == Short.class && Short.MIN_VALUE <= intValue && intValue <= Short.MAX_VALUE) { - value = (short) intValue; - } else if (requiredType == Byte.class && Byte.MIN_VALUE <= intValue && intValue <= Byte.MAX_VALUE) { - value = (byte) intValue; + if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { + long longValue = ((Number) value).longValue(); + if (Long.class.equals(requiredType)) { + value = longValue; + } else if (Integer.class.equals(requiredType) && Integer.MIN_VALUE <= longValue && longValue <= Integer.MAX_VALUE) { + value = (int) longValue; + } else if (requiredType == Short.class && Short.MIN_VALUE <= longValue && longValue <= Short.MAX_VALUE) { + value = (short) longValue; + } else if (requiredType == Byte.class && Byte.MIN_VALUE <= longValue && longValue <= Byte.MAX_VALUE) { + value = (byte) longValue; } } + if (value instanceof Long && + (requiredType.equals(Integer.class) || requiredType.equals(Short.class) || requiredType.equals(Byte.class))) { + String msg = "Claim '" + name + "' value is too large or too small to be represented as a " + + requiredType.getName() + " instance (would cause numeric overflow)."; + throw new RequiredTypeException(msg); + } + if (!requiredType.isInstance(value)) { throw new RequiredTypeException(String.format(CONVERSION_ERROR_MSG, value.getClass(), requiredType)); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultDynamicHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultDynamicHeaderBuilder.java new file mode 100644 index 00000000..38ab0171 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultDynamicHeaderBuilder.java @@ -0,0 +1,212 @@ +/* + * 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.DynamicHeaderBuilder; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.ProtectedHeader; +import io.jsonwebtoken.impl.security.DefaultX509Builder; +import io.jsonwebtoken.security.PublicJwk; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultDynamicHeaderBuilder implements DynamicHeaderBuilder { + + private Header header; + + private DefaultX509Builder x509Builder; + + public DefaultDynamicHeaderBuilder() { + this.header = new DefaultUnprotectedHeader(); + this.x509Builder = null; + } + + private ProtectedHeader ensureProtected() { + ProtectedHeader ph; + if (this.header instanceof ProtectedHeader) { + ph = (ProtectedHeader) this.header; + } else { + this.header = ph = new DefaultJwsHeader(this.header); + this.x509Builder = new DefaultX509Builder(ph, this, IllegalStateException.class); + } + return ph; + } + + private JweHeader ensureJwe() { + JweHeader h; + if (this.header instanceof JweHeader) { + h = (JweHeader) this.header; + } else { + this.header = h = new DefaultJweHeader(this.header); + this.x509Builder = new DefaultX509Builder(h, this, IllegalStateException.class); + } + return h; + } + + @Override + public DynamicHeaderBuilder put(String key, Object value) { + this.header.put(key, value); + return this; + } + + @Override + public DynamicHeaderBuilder remove(String key) { + this.header.remove(key); + return this; + } + + @Override + public DynamicHeaderBuilder putAll(Map m) { + this.header.putAll(m); + return this; + } + + @Override + public DynamicHeaderBuilder clear() { + this.header.clear(); + return this; + } + + @Override + public DynamicHeaderBuilder setType(String typ) { + this.header.setType(typ); + return this; + } + + @Override + public DynamicHeaderBuilder setContentType(String cty) { + this.header.setContentType(cty); + return this; + } + + @Override + public DynamicHeaderBuilder setAlgorithm(String alg) { + this.header.setAlgorithm(alg); + return this; + } + + @Override + public DynamicHeaderBuilder setCompressionAlgorithm(String zip) { + this.header.setCompressionAlgorithm(zip); + return this; + } + + @Override + public DynamicHeaderBuilder setJwkSetUrl(URI uri) { + ensureProtected().setJwkSetUrl(uri); + return this; + } + + @Override + public DynamicHeaderBuilder setJwk(PublicJwk jwk) { + ensureProtected().setJwk(jwk); + return this; + } + + @Override + public DynamicHeaderBuilder setKeyId(String kid) { + ensureProtected().setKeyId(kid); + return this; + } + + @Override + public DynamicHeaderBuilder setCritical(Set crit) { + ensureProtected().setCritical(crit); + return this; + } + + @Override + public DynamicHeaderBuilder withX509Sha1Thumbprint(boolean enable) { + ensureProtected(); + return this.x509Builder.withX509Sha1Thumbprint(enable); + } + + @Override + public DynamicHeaderBuilder withX509Sha256Thumbprint(boolean enable) { + ensureProtected(); + return this.x509Builder.withX509Sha256Thumbprint(enable); + } + + @Override + public DynamicHeaderBuilder setX509Url(URI uri) { + ensureProtected(); + return this.x509Builder.setX509Url(uri); + } + + @Override + public DynamicHeaderBuilder setX509CertificateChain(List chain) { + ensureProtected(); + return this.x509Builder.setX509CertificateChain(chain); + } + + @Override + public DynamicHeaderBuilder setX509CertificateSha1Thumbprint(byte[] thumbprint) { + ensureProtected(); + return this.x509Builder.setX509CertificateSha1Thumbprint(thumbprint); + } + + @Override + public DynamicHeaderBuilder setX509CertificateSha256Thumbprint(byte[] thumbprint) { + ensureProtected(); + return this.x509Builder.setX509CertificateSha256Thumbprint(thumbprint); + } + + @Override + public DynamicHeaderBuilder setAgreementPartyUInfo(byte[] info) { + ensureJwe().setAgreementPartyUInfo(info); + return this; + } + + @Override + public DynamicHeaderBuilder setAgreementPartyUInfo(String info) { + ensureJwe().setAgreementPartyUInfo(info); + return this; + } + + @Override + public DynamicHeaderBuilder setAgreementPartyVInfo(byte[] info) { + ensureJwe().setAgreementPartyVInfo(info); + return this; + } + + @Override + public DynamicHeaderBuilder setAgreementPartyVInfo(String info) { + ensureJwe().setAgreementPartyVInfo(info); + return this; + } + + @Override + public DynamicHeaderBuilder setPbes2Count(int count) { + ensureJwe().setPbes2Count(count); + return this; + } + + @Override + public Header build() { + if (this.x509Builder != null) { + this.x509Builder.apply(); + } + return this.header; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java deleted file mode 100644 index 3560afc4..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2014 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.Header; -import io.jsonwebtoken.lang.Strings; - -import java.util.Map; - -@SuppressWarnings("unchecked") -public class DefaultHeader> extends JwtMap implements Header { - - public DefaultHeader() { - super(); - } - - public DefaultHeader(Map map) { - super(map); - } - - @Override - public String getType() { - return getString(TYPE); - } - - @Override - public T setType(String typ) { - setValue(TYPE, typ); - return (T)this; - } - - @Override - public String getContentType() { - return getString(CONTENT_TYPE); - } - - @Override - public T setContentType(String cty) { - setValue(CONTENT_TYPE, cty); - return (T)this; - } - - @SuppressWarnings("deprecation") - @Override - public String getCompressionAlgorithm() { - String alg = getString(COMPRESSION_ALGORITHM); - if (!Strings.hasText(alg)) { - alg = getString(DEPRECATED_COMPRESSION_ALGORITHM); - } - return alg; - } - - @Override - public T setCompressionAlgorithm(String compressionAlgorithm) { - setValue(COMPRESSION_ALGORITHM, compressionAlgorithm); - return (T) this; - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java new file mode 100644 index 00000000..32467fda --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwe.java @@ -0,0 +1,71 @@ +/* + * 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.Jwe; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; + +public class DefaultJwe

extends DefaultJwt implements Jwe

{ + + private final byte[] iv; + private final byte[] aadTag; + + public DefaultJwe(JweHeader header, P body, byte[] iv, byte[] aadTag) { + super(header, body); + this.iv = Assert.notEmpty(iv, "Initialization vector cannot be null or empty."); + this.aadTag = Assert.notEmpty(aadTag, "AAD tag cannot be null or empty."); + } + + @Override + public byte[] getInitializationVector() { + return this.iv; + } + + @Override + public byte[] getAadTag() { + return this.aadTag; + } + + @Override + protected StringBuilder toStringBuilder() { + StringBuilder sb = super.toStringBuilder(); + sb.append(",iv=").append(Encoders.BASE64URL.encode(this.iv)); + sb.append(",tag=").append(Encoders.BASE64URL.encode(this.aadTag)); + return sb; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Jwe) { + Jwe jwe = (Jwe) obj; + return super.equals(jwe) && + Objects.nullSafeEquals(iv, jwe.getInitializationVector()) && + Objects.nullSafeEquals(aadTag, jwe.getAadTag()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(getHeader(), getPayload(), iv, aadTag); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java new file mode 100644 index 00000000..f8235dc9 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java @@ -0,0 +1,146 @@ +/* + * 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.JweHeader; +import io.jsonwebtoken.impl.lang.Converters; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.PositiveIntegerConverter; +import io.jsonwebtoken.impl.lang.RequiredBitLengthConverter; +import io.jsonwebtoken.impl.security.JwkConverter; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.PublicJwk; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; + +/** + * Header implementation satisfying JWE header parameter requirements. + * + * @since JJWT_RELEASE_VERSION + */ +public class DefaultJweHeader extends AbstractProtectedHeader implements JweHeader { + + static final Field ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm"); + + @SuppressWarnings("unchecked") + public static final Field> EPK = Fields.builder((Class>) (Class) PublicJwk.class) + .setId("epk").setName("Ephemeral Public Key") + .setConverter(JwkConverter.PUBLIC_JWK).build(); + static final Field APU = Fields.bytes("apu", "Agreement PartyUInfo").build(); + static final Field APV = Fields.bytes("apv", "Agreement PartyVInfo").build(); + + // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.7.1.1 says 96 bits required: + public static final Field IV = Fields.bytes("iv", "Initialization Vector") + .setConverter(new RequiredBitLengthConverter(Converters.BASE64URL_BYTES, 96)).build(); + + // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.7.1.2 says 128 bits required: + public static final Field TAG = Fields.bytes("tag", "Authentication Tag") + .setConverter(new RequiredBitLengthConverter(Converters.BASE64URL_BYTES, 128)).build(); + + // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.1 says at least 64 bits (8 bytes) is required: + public static final Field P2S = Fields.bytes("p2s", "PBES2 Salt Input") + .setConverter(new RequiredBitLengthConverter(Converters.BASE64URL_BYTES, 64, false)).build(); + public static final Field P2C = Fields.builder(Integer.class) + .setConverter(PositiveIntegerConverter.INSTANCE).setId("p2c").setName("PBES2 Count").build(); + + static final Set> FIELDS = Collections.concat(AbstractProtectedHeader.FIELDS, ENCRYPTION_ALGORITHM, EPK, APU, APV, IV, TAG, P2S, P2C); + + public DefaultJweHeader() { + super(FIELDS); + } + + public DefaultJweHeader(Map map) { + super(FIELDS, map); + } + + @Override + public String getName() { + return "JWE header"; + } + + @Override + public String getEncryptionAlgorithm() { + return idiomaticGet(ENCRYPTION_ALGORITHM); + } + + @Override + public PublicJwk getEphemeralPublicKey() { + return idiomaticGet(EPK); + } + + @Override + public byte[] getAgreementPartyUInfo() { + return idiomaticGet(APU); + } + + @Override + public JweHeader setAgreementPartyUInfo(byte[] info) { + put(APU, info); + return this; + } + + @Override + public JweHeader setAgreementPartyUInfo(String info) { + byte[] bytes = Strings.hasText(info) ? info.getBytes(StandardCharsets.UTF_8) : null; + return setAgreementPartyUInfo(bytes); + } + + @Override + public byte[] getAgreementPartyVInfo() { + return idiomaticGet(APV); + } + + @Override + public JweHeader setAgreementPartyVInfo(byte[] info) { + put(APV, info); + return this; + } + + @Override + public JweHeader setAgreementPartyVInfo(String info) { + byte[] bytes = Strings.hasText(info) ? info.getBytes(StandardCharsets.UTF_8) : null; + return setAgreementPartyVInfo(bytes); + } + + @Override + public byte[] getInitializationVector() { + return idiomaticGet(IV); + } + + @Override + public byte[] getAuthenticationTag() { + return idiomaticGet(TAG); + } + + public byte[] getPbes2Salt() { + return idiomaticGet(P2S); + } + + @Override + public Integer getPbes2Count() { + return idiomaticGet(P2C); + } + + @Override + public JweHeader setPbes2Count(int count) { + put(P2C, count); + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderBuilder.java new file mode 100644 index 00000000..8ad2de07 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderBuilder.java @@ -0,0 +1,60 @@ +/* + * 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.JweHeader; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultJweHeaderBuilder extends AbstractProtectedHeaderBuilder + implements JweHeaderBuilder { + + @Override + protected JweHeader newHeader() { + return new DefaultJweHeader(); + } + + @Override + public JweHeaderBuilder setAgreementPartyUInfo(byte[] info) { + this.header.setAgreementPartyUInfo(info); + return this; + } + + @Override + public JweHeaderBuilder setAgreementPartyUInfo(String info) { + this.header.setAgreementPartyUInfo(info); + return this; + } + + @Override + public JweHeaderBuilder setAgreementPartyVInfo(byte[] info) { + this.header.setAgreementPartyVInfo(info); + return this; + } + + @Override + public JweHeaderBuilder setAgreementPartyVInfo(String info) { + this.header.setAgreementPartyVInfo(info); + return this; + } + + @Override + public JweHeaderBuilder setPbes2Count(int count) { + this.header.setPbes2Count(count); + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java index fe83244c..2d5e320d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java @@ -17,36 +17,42 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.lang.Objects; -public class DefaultJws implements Jws { +public class DefaultJws

extends DefaultJwt implements Jws

{ - private final JwsHeader header; - private final B body; private final String signature; - public DefaultJws(JwsHeader header, B body, String signature) { - this.header = header; - this.body = body; + public DefaultJws(JwsHeader header, P body, String signature) { + super(header, body); this.signature = signature; } - @Override - public JwsHeader getHeader() { - return this.header; - } - - @Override - public B getBody() { - return this.body; - } - @Override public String getSignature() { return this.signature; } @Override - public String toString() { - return "header=" + header + ",body=" + body + ",signature=" + signature; + protected StringBuilder toStringBuilder() { + return super.toStringBuilder().append(",signature=").append(signature); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Jws) { + Jws jws = (Jws) obj; + return super.equals(jws) && + Objects.nullSafeEquals(signature, jws.getSignature()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(getHeader(), getPayload(), signature); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java index cb747e3d..0d4d3a7c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeader.java @@ -16,39 +16,25 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.impl.lang.Field; import java.util.Map; +import java.util.Set; -public class DefaultJwsHeader extends DefaultHeader implements JwsHeader { +public class DefaultJwsHeader extends AbstractProtectedHeader implements JwsHeader { + + static final Set> FIELDS = AbstractProtectedHeader.FIELDS; //same public DefaultJwsHeader() { - super(); + super(FIELDS); } - public DefaultJwsHeader(Map map) { - super(map); + public DefaultJwsHeader(Map map) { + super(FIELDS, map); } @Override - public String getAlgorithm() { - return getString(ALGORITHM); + public String getName() { + return "JWS header"; } - - @Override - public JwsHeader setAlgorithm(String alg) { - setValue(ALGORITHM, alg); - return this; - } - - @Override - public String getKeyId() { - return getString(KEY_ID); - } - - @Override - public JwsHeader setKeyId(String kid) { - setValue(KEY_ID, kid); - return this; - } - } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeaderBuilder.java new file mode 100644 index 00000000..3640fa26 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwsHeaderBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.JwsHeader; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultJwsHeaderBuilder extends AbstractProtectedHeaderBuilder + implements JwsHeaderBuilder { + + @Override + protected JwsHeader newHeader() { + return new DefaultJwsHeader(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java index e09bd006..560fea09 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java @@ -17,29 +17,67 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; -public class DefaultJwt implements Jwt { +public class DefaultJwt, P> implements Jwt { - private final Header header; - private final B body; + private final H header; + private final P payload; - public DefaultJwt(Header header, B body) { - this.header = header; - this.body = body; + public DefaultJwt(H header, P payload) { + this.header = Assert.notNull(header, "header cannot be null."); + this.payload = Assert.notNull(payload, "payload cannot be null."); } @Override - public Header getHeader() { + public H getHeader() { return header; } @Override - public B getBody() { - return body; + public P getBody() { + return getPayload(); } @Override - public String toString() { - return "header=" + header + ",body=" + body; + public P getPayload() { + return this.payload; + } + + protected StringBuilder toStringBuilder() { + StringBuilder sb = new StringBuilder(100); + sb.append("header=").append(header).append(",payload="); + if (payload instanceof byte[]) { + String encoded = Encoders.BASE64URL.encode((byte[]) payload); + sb.append(encoded); + } else { + sb.append(payload); + } + return sb; + } + + @Override + public final String toString() { + return toStringBuilder().toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Jwt) { + Jwt jwt = (Jwt) obj; + return Objects.nullSafeEquals(header, jwt.getHeader()) && + Objects.nullSafeEquals(payload, jwt.getPayload()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(header, payload); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 2c1a0a3a..66a7aa06 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -18,48 +18,120 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Claims; import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; -import io.jsonwebtoken.impl.crypto.JwtSigner; -import io.jsonwebtoken.impl.lang.LegacyServices; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CompactMediaTypeIdConverter; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.Functions; +import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.impl.security.DefaultAeadRequest; +import io.jsonwebtoken.impl.security.DefaultKeyRequest; +import io.jsonwebtoken.impl.security.DefaultSecureRequest; +import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Builder; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SignatureException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; import java.util.Date; import java.util.Map; public class DefaultJwtBuilder implements JwtBuilder { - private Header header; - private Claims claims; - private String payload; + public static final String PUB_KEY_SIGN_MSG = "PublicKeys may not be used to create digital signatures. " + + "Only PrivateKeys may be used to create digital signatures, and PublicKeys are used to verify " + + "digital signatures."; + + protected Provider provider; + protected SecureRandom secureRandom; + + protected Header header; + protected Claims claims; + protected byte[] content; + + private SecureDigestAlgorithm sigAlg = Jwts.SIG.NONE; + private Function, byte[]> signFunction; + + private AeadAlgorithm enc; // MUST be Symmetric AEAD per https://tools.ietf.org/html/rfc7516#section-4.1.2 + private Function encFunction; + + private KeyAlgorithm keyAlg; + private Function, KeyResult> keyAlgFunction; - private SignatureAlgorithm algorithm; private Key key; - private Serializer> serializer; + protected Serializer> serializer; + protected Function, byte[]> headerSerializer; + protected Function, byte[]> claimsSerializer; - private Encoder base64UrlEncoder = Encoders.BASE64URL; - - private CompressionCodec compressionCodec; + protected Encoder base64UrlEncoder = Encoders.BASE64URL; + protected CompressionCodec compressionCodec; @Override - public JwtBuilder serializeToJsonWith(Serializer> serializer) { + public JwtBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public JwtBuilder setSecureRandom(SecureRandom secureRandom) { + this.secureRandom = secureRandom; + return this; + } + + protected Function wrap(Function fn, String fmt, Object... args) { + return Functions.wrap(fn, SecurityException.class, fmt, args); + } + + protected Function, byte[]> wrap(final Serializer> serializer, final String which) { + return new Function, byte[]>() { + @Override + public byte[] apply(Map stringMap) { + try { + return serializer.serialize(stringMap); + } catch (Exception e) { + String fmt = String.format("Unable to serialize %s to JSON.", which); + String msg = fmt + " Cause: " + e.getMessage(); + throw new SerializationException(msg); + } + } + }; + } + + @Override + public JwtBuilder serializeToJsonWith(final Serializer> serializer) { Assert.notNull(serializer, "Serializer cannot be null."); this.serializer = serializer; + this.headerSerializer = wrap(serializer, "header"); + this.claimsSerializer = wrap(serializer, "claims"); return this; } @@ -71,33 +143,35 @@ public class DefaultJwtBuilder implements JwtBuilder { } @Override - public JwtBuilder setHeader(Header header) { - this.header = header; + public JwtBuilder setHeader(Header header) { + return setHeader(Jwts.header().putAll(header)); + } + + @Override + public JwtBuilder setHeader(Map header) { + return setHeader(Jwts.header().putAll(header)); + } + + @Override + public JwtBuilder setHeader(Builder> builder) { + Assert.notNull(builder, "Builder cannot be null."); + Header header = builder.build(); + this.header = Assert.notNull(header, "Builder cannot produce a null Header instance."); return this; } @Override - public JwtBuilder setHeader(Map header) { - this.header = new DefaultHeader(header); - return this; - } - - @Override - public JwtBuilder setHeaderParams(Map params) { + public JwtBuilder setHeaderParams(Map params) { if (!Collections.isEmpty(params)) { - - Header header = ensureHeader(); - - for (Map.Entry entry : params.entrySet()) { - header.put(entry.getKey(), entry.getValue()); - } + Header header = ensureHeader(); + header.putAll(params); } return this; } - protected Header ensureHeader() { + protected Header ensureHeader() { if (this.header == null) { - this.header = new DefaultHeader(); + this.header = new DefaultUnprotectedHeader(); } return this.header; } @@ -108,26 +182,69 @@ public class DefaultJwtBuilder implements JwtBuilder { return this; } + @SuppressWarnings("unchecked") // TODO: remove for 1.0 + protected static SecureDigestAlgorithm forSigningKey(K key) { + @SuppressWarnings("deprecation") + io.jsonwebtoken.SignatureAlgorithm alg = io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key); + return (SecureDigestAlgorithm) Jwts.SIG.get(alg.getValue()); + } + @Override public JwtBuilder signWith(Key key) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); - SignatureAlgorithm alg = SignatureAlgorithm.forSigningKey(key); + SecureDigestAlgorithm alg = forSigningKey(key); // https://github.com/jwtk/jjwt/issues/381 return signWith(key, alg); } @Override - public JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException { + public JwtBuilder signWith(K key, final SecureDigestAlgorithm alg) throws InvalidKeyException { Assert.notNull(key, "Key argument cannot be null."); + if (key instanceof PublicKey) { // it's always wrong to try to create signatures with PublicKeys: + throw new IllegalArgumentException(PUB_KEY_SIGN_MSG); + } + // Implementation note: Ordinarily Passwords should not be used to create secure digests because they usually + // lack the length or entropy necessary for secure cryptographic operations, and are prone to misuse. + // However, we DO NOT prevent them as arguments here (like the above PublicKey check) because + // it is conceivable that a custom SecureDigestAlgorithm implementation would allow Password instances + // so that it might perform its own internal key-derivation logic producing a key that is then used to create a + // secure hash. + // + // Even so, a fallback safety check is that JJWT's only out-of-the-box Password implementation + // (io.jsonwebtoken.impl.security.PasswordSpec) explicitly forbids calls to password.getEncoded() in all + // scenarios to avoid potential misuse, so a digest algorithm implementation would explicitly need to avoid + // this by calling toCharArray() instead. + // + // TLDR; the digest algorithm implementation has the final say whether a password instance is valid + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334 - createSigner(alg, key); // since 0.11.5: fail fast if key cannot be used for alg. - this.algorithm = alg; + String id = Assert.hasText(alg.getId(), "SignatureAlgorithm id cannot be null or empty."); + if (Jwts.SIG.NONE.getId().equalsIgnoreCase(id)) { + String msg = "The 'none' JWS algorithm cannot be used to sign JWTs."; + throw new IllegalArgumentException(msg); + } this.key = key; + //noinspection unchecked + this.sigAlg = (SecureDigestAlgorithm) alg; + this.signFunction = Functions.wrap(new Function, byte[]>() { + @Override + public byte[] apply(SecureRequest request) { + return sigAlg.digest(request); + } + }, SignatureException.class, "Unable to compute %s signature.", id); return this; } + @SuppressWarnings({"deprecation", "unchecked"}) // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException { + public JwtBuilder signWith(Key key, io.jsonwebtoken.SignatureAlgorithm alg) throws InvalidKeyException { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334 + return signWith(key, (SecureDigestAlgorithm) Jwts.SIG.get(alg.getValue())); + } + + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 + @Override + public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); @@ -135,19 +252,57 @@ public class DefaultJwtBuilder implements JwtBuilder { return signWith(key, alg); } + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(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.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); return signWith(alg, bytes); } + @SuppressWarnings("deprecation") // TODO: remove method for 1.0 @Override - public JwtBuilder signWith(SignatureAlgorithm alg, Key key) { + public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, Key key) { return signWith(key, alg); } + @Override + public JwtBuilder encryptWith(SecretKey key, AeadAlgorithm enc) { + if (key instanceof Password) { + return encryptWith((Password) key, new Pbes2HsAkwAlgorithm(enc.getKeyBitLength()), enc); + } + return encryptWith(key, Jwts.KEY.DIRECT, enc); + } + + @Override + public JwtBuilder encryptWith(final K key, final KeyAlgorithm keyAlg, final AeadAlgorithm enc) { + this.enc = Assert.notNull(enc, "Encryption algorithm cannot be null."); + final String encId = Assert.hasText(enc.getId(), "Encryption algorithm id cannot be null or empty."); + this.encFunction = wrap(new Function() { + @Override + public AeadResult apply(AeadRequest request) { + return enc.encrypt(request); + } + }, "%s encryption failed.", encId); + + this.key = Assert.notNull(key, "Key cannot be null."); + + //noinspection unchecked + this.keyAlg = (KeyAlgorithm) Assert.notNull(keyAlg, "KeyAlgorithm cannot be null."); + final String algId = Assert.hasText(keyAlg.getId(), "KeyAlgorithm id cannot be null or empty."); + final KeyAlgorithm alg = this.keyAlg; + final String cekMsg = "Unable to obtain content encryption key from key management algorithm '%s'."; + this.keyAlgFunction = Functions.wrap(new Function, KeyResult>() { + @Override + public KeyResult apply(KeyRequest request) { + return alg.getEncryptionKey(request); + } + }, SecurityException.class, cekMsg, algId); + + return this; + } + @Override public JwtBuilder compressWith(CompressionCodec compressionCodec) { Assert.notNull(compressionCodec, "compressionCodec cannot be null"); @@ -157,10 +312,25 @@ public class DefaultJwtBuilder implements JwtBuilder { @Override public JwtBuilder setPayload(String payload) { - this.payload = payload; + byte[] bytes = payload != null ? payload.getBytes(StandardCharsets.UTF_8) : null; + return setContent(bytes); + } + + @Override + public JwtBuilder setContent(byte[] content) { + this.content = content; return this; } + @Override + public JwtBuilder setContent(byte[] content, String cty) { + Assert.notEmpty(content, "content byte array cannot be null or empty."); + Assert.hasText(cty, "Content Type String cannot be null or empty."); + cty = CompactMediaTypeIdConverter.INSTANCE.applyFrom(cty); + ensureHeader().setContentType(cty); + return setContent(content); + } + protected Claims ensureClaims() { if (this.claims == null) { this.claims = new DefaultClaims(); @@ -181,101 +351,52 @@ public class DefaultJwtBuilder implements JwtBuilder { } @Override - public JwtBuilder addClaims(Map claims) { + public JwtBuilder addClaims(Map claims) { ensureClaims().putAll(claims); return this; } @Override public JwtBuilder setIssuer(String iss) { - if (Strings.hasText(iss)) { - ensureClaims().setIssuer(iss); - } else { - if (this.claims != null) { - claims.setIssuer(iss); - } - } - return this; + return claim(DefaultClaims.ISSUER.getId(), iss); } @Override public JwtBuilder setSubject(String sub) { - if (Strings.hasText(sub)) { - ensureClaims().setSubject(sub); - } else { - if (this.claims != null) { - claims.setSubject(sub); - } - } - return this; + return claim(DefaultClaims.SUBJECT.getId(), sub); } @Override public JwtBuilder setAudience(String aud) { - if (Strings.hasText(aud)) { - ensureClaims().setAudience(aud); - } else { - if (this.claims != null) { - claims.setAudience(aud); - } - } - return this; + return claim(DefaultClaims.AUDIENCE.getId(), aud); } @Override public JwtBuilder setExpiration(Date exp) { - if (exp != null) { - ensureClaims().setExpiration(exp); - } else { - if (this.claims != null) { - //noinspection ConstantConditions - this.claims.setExpiration(exp); - } - } - return this; + return claim(DefaultClaims.EXPIRATION.getId(), exp); } @Override public JwtBuilder setNotBefore(Date nbf) { - if (nbf != null) { - ensureClaims().setNotBefore(nbf); - } else { - if (this.claims != null) { - //noinspection ConstantConditions - this.claims.setNotBefore(nbf); - } - } - return this; + return claim(DefaultClaims.NOT_BEFORE.getId(), nbf); } @Override public JwtBuilder setIssuedAt(Date iat) { - if (iat != null) { - ensureClaims().setIssuedAt(iat); - } else { - if (this.claims != null) { - //noinspection ConstantConditions - this.claims.setIssuedAt(iat); - } - } - return this; + return claim(DefaultClaims.ISSUED_AT.getId(), iat); } @Override public JwtBuilder setId(String jti) { - if (Strings.hasText(jti)) { - ensureClaims().setId(jti); - } else { - if (this.claims != null) { - claims.setId(jti); - } - } - return this; + return claim(DefaultClaims.JTI.getId(), jti); } @Override public JwtBuilder claim(String name, Object value) { Assert.hasText(name, "Claim property name cannot be null or empty."); + if (value instanceof String && !Strings.hasText((String) value)) { + value = null; + } if (this.claims == null) { if (value != null) { ensureClaims().put(name, value); @@ -287,108 +408,126 @@ public class DefaultJwtBuilder implements JwtBuilder { this.claims.put(name, value); } } - return this; } @Override public String compact() { - if (this.serializer == null) { - // try to find one based on the services available - // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 - // use the previous commented out line instead - this.serializer = LegacyServices.loadFirst(Serializer.class); + final boolean jwe = encFunction != null; + + if (jwe && signFunction != null) { + String msg = "Both 'signWith' and 'encryptWith' cannot be specified - choose either."; + throw new IllegalStateException(msg); } - if (payload == null && Collections.isEmpty(claims)) { - payload = ""; + if (Objects.isEmpty(content) && Collections.isEmpty(claims)) { + if (jwe) { // JWE payload can never be empty: + String msg = "Encrypted JWTs must have either 'claims' or non-empty 'content'."; + throw new IllegalStateException(msg); + } else { //JWS or Unprotected JWT payloads can be empty + content = Bytes.EMPTY; + } + } + if (!Objects.isEmpty(content) && !Collections.isEmpty(claims)) { + throw new IllegalStateException("Both 'content' and 'claims' cannot both be specified. Choose either one."); } - if (payload != null && !Collections.isEmpty(claims)) { - throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one."); - } + Header header = ensureHeader(); - Header header = ensureHeader(); - - JwsHeader jwsHeader; - if (header instanceof JwsHeader) { - jwsHeader = (JwsHeader) header; - } else { + if (this.serializer == null) { // try to find one based on the services available //noinspection unchecked - jwsHeader = new DefaultJwsHeader(header); + serializeToJsonWith(Services.loadFirst(Serializer.class)); } - if (key != null) { - jwsHeader.setAlgorithm(algorithm.getValue()); + byte[] payload = content; + if (!Collections.isEmpty(claims)) { + payload = claimsSerializer.apply(claims); + } + if (!Objects.isEmpty(payload) && compressionCodec != null) { + payload = compressionCodec.compress(payload); + header.setCompressionAlgorithm(compressionCodec.getId()); + } + + if (jwe) { + JweHeader jweHeader = header instanceof JweHeader ? (JweHeader) header : new DefaultJweHeader(header); + return encrypt(jweHeader, payload); } else { - //no signature - plaintext JWT: - jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue()); + return compact(header, payload); + } + } + + private String compact(Header header, byte[] payload) { + + Assert.stateNotNull(sigAlg, "SignatureAlgorithm is required."); // invariant + + if (this.key != null && !(header instanceof JwsHeader)) { + header = new DefaultJwsHeader(header); } - if (compressionCodec != null) { - jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); - } + header.setAlgorithm(sigAlg.getId()); - String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); + byte[] headerBytes = headerSerializer.apply(header); + String base64UrlEncodedHeader = base64UrlEncoder.encode(headerBytes); + String base64UrlEncodedBody = base64UrlEncoder.encode(payload); - byte[] bytes; - try { - bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims); - } catch (SerializationException e) { - throw new IllegalArgumentException("Unable to serialize claims object to json: " + e.getMessage(), e); - } + String jwt = base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; - if (compressionCodec != null) { - bytes = compressionCodec.compress(bytes); - } - - String base64UrlEncodedBody = base64UrlEncoder.encode(bytes); - - String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; - - if (key != null) { //jwt must be signed: - - JwtSigner signer = createSigner(algorithm, key); - - String base64UrlSignature = signer.sign(jwt); - - jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature; + if (this.key != null) { //jwt must be signed: + Assert.stateNotNull(key, "Signing key cannot be null."); + Assert.stateNotNull(signFunction, "signFunction cannot be null."); + byte[] data = jwt.getBytes(StandardCharsets.US_ASCII); + SecureRequest request = new DefaultSecureRequest<>(data, provider, secureRandom, key); + byte[] signature = signFunction.apply(request); + String base64UrlSignature = base64UrlEncoder.encode(signature); + jwt += DefaultJwtParser.SEPARATOR_CHAR + base64UrlSignature; } else { - // no signature (plaintext), but must terminate w/ a period, see - // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 - jwt += JwtParser.SEPARATOR_CHAR; + // no signature (unprotected JWT), but must terminate w/ a period, see + // https://www.rfc-editor.org/rfc/rfc7519#section-6.1 + jwt += DefaultJwtParser.SEPARATOR_CHAR; } return jwt; } - /* - * @since 0.5 mostly to allow testing overrides - */ - protected JwtSigner createSigner(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSigner(alg, key, base64UrlEncoder); - } + private String encrypt(JweHeader header, byte[] payload) { - @Deprecated // remove before 1.0 - call the serializer and base64UrlEncoder directly - protected String base64UrlEncode(Object o, String errMsg) { - Assert.isInstanceOf(Map.class, o, "object argument must be a map."); - Map m = (Map)o; - byte[] bytes; - try { - bytes = toJson(m); - } catch (SerializationException e) { - throw new IllegalStateException(errMsg, e); - } + Assert.stateNotNull(key, "Key is required."); // set by encryptWith* + Assert.stateNotNull(enc, "Encryption algorithm is required."); // set by encryptWith* + Assert.stateNotNull(encFunction, "Encryption function cannot be null."); + Assert.stateNotNull(keyAlg, "KeyAlgorithm is required."); //set by encryptWith* + Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null."); + Assert.notEmpty(payload, "JWE payload bytes cannot be empty."); // JWE invariant (JWS can be empty however) - return base64UrlEncoder.encode(bytes); - } + KeyRequest keyRequest = new DefaultKeyRequest<>(this.key, this.provider, this.secureRandom, header, enc); + KeyResult keyResult = keyAlgFunction.apply(keyRequest); + Assert.stateNotNull(keyResult, "KeyAlgorithm must return a KeyResult."); + SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key."); + byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty."); - @SuppressWarnings("unchecked") - @Deprecated //remove before 1.0 - call the serializer directly - protected byte[] toJson(Object object) throws SerializationException { - Assert.isInstanceOf(Map.class, object, "object argument must be a map."); - Map m = (Map)object; - return serializer.serialize(m); + header.put(AbstractHeader.ALGORITHM.getId(), keyAlg.getId()); + header.put(DefaultJweHeader.ENCRYPTION_ALGORITHM.getId(), enc.getId()); + + byte[] headerBytes = this.headerSerializer.apply(header); + final String base64UrlEncodedHeader = base64UrlEncoder.encode(headerBytes); + byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII); + + AeadRequest encRequest = new DefaultAeadRequest(payload, provider, secureRandom, cek, aad); + AeadResult encResult = encFunction.apply(encRequest); + + byte[] iv = Assert.notEmpty(encResult.getInitializationVector(), "Encryption result must have a non-empty initialization vector."); + byte[] ciphertext = Assert.notEmpty(encResult.getPayload(), "Encryption result must have non-empty ciphertext (result.getData())."); + byte[] tag = Assert.notEmpty(encResult.getDigest(), "Encryption result must have a non-empty authentication tag."); + + String base64UrlEncodedEncryptedCek = base64UrlEncoder.encode(encryptedCek); + String base64UrlEncodedIv = base64UrlEncoder.encode(iv); + String base64UrlEncodedCiphertext = base64UrlEncoder.encode(ciphertext); + String base64UrlEncodedTag = base64UrlEncoder.encode(tag); + + return base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedEncryptedCek + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedIv + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedCiphertext + DefaultJwtParser.SEPARATOR_CHAR + + base64UrlEncodedTag; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 0feaa3ea..78b58e50 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -15,60 +15,170 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.IncorrectClaimException; -import io.jsonwebtoken.InvalidClaimException; +import io.jsonwebtoken.Jwe; +import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtHandlerAdapter; import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Locator; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.PrematureJwtException; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.UnprotectedHeader; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; -import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; -import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.impl.lang.LegacyServices; +import io.jsonwebtoken.impl.security.ConstantKeyLocator; +import io.jsonwebtoken.impl.security.DefaultAeadResult; +import io.jsonwebtoken.impl.security.DefaultDecryptionKeyRequest; +import io.jsonwebtoken.impl.security.DefaultVerifySecureDigestRequest; +import io.jsonwebtoken.impl.security.LocatingKeyResolver; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.DecodingException; +import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.DateFormats; -import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptAeadRequest; +import io.jsonwebtoken.security.DecryptionKeyRequest; import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.Message; +import io.jsonwebtoken.security.SecureDigestAlgorithm; import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.VerifySecureDigestRequest; import io.jsonwebtoken.security.WeakKeyException; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.Provider; +import java.util.Collection; import java.util.Date; +import java.util.LinkedHashSet; import java.util.Map; @SuppressWarnings("unchecked") public class DefaultJwtParser implements JwtParser { + static final char SEPARATOR_CHAR = '.'; + private static final int MILLISECONDS_PER_SECOND = 1000; - // TODO: make the folling fields final for v1.0 - private byte[] keyBytes; + private static final JwtTokenizer jwtTokenizer = new JwtTokenizer(); - private Key key; + public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not " + + "present in the JWT claims."; + public static final String MISSING_JWS_ALG_MSG = "JWS header does not contain a required 'alg' (Algorithm) " + + "header parameter. This header parameter is mandatory per the JWS Specification, Section 4.1.1. See " + + "https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.1 for more information."; + + public static final String MISSING_JWE_ALG_MSG = "JWE header does not contain a required 'alg' (Algorithm) " + + "header parameter. This header parameter is mandatory per the JWE Specification, Section 4.1.1. See " + + "https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.1 for more information."; + + public static final String MISSING_JWS_DIGEST_MSG_FMT = "The JWS header references signature algorithm '%s' but " + + "the compact JWE string is missing the required signature."; + + public static final String MISSING_JWE_DIGEST_MSG_FMT = "The JWE header references key management algorithm '%s' " + + "but the compact JWE string is missing the " + "required AAD authentication tag."; + + private static final String MISSING_ENC_MSG = "JWE header does not contain a required " + + "'enc' (Encryption Algorithm) header parameter. " + "This header parameter is mandatory per the JWE " + + "Specification, Section 4.1.2. See " + + "https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.2 for more information."; + + private static final String UNSECURED_DISABLED_MSG_PREFIX = "Unsecured JWSs (those with an " + + AbstractHeader.ALGORITHM + " header value of '" + Jwts.SIG.NONE.getId() + "') are disallowed by " + + "default as mandated by https://www.rfc-editor.org/rfc/rfc7518.html#section-3.6. If you wish to " + + "allow them to be parsed, call the JwtParserBuilder.enableUnsecuredJws() method (but please read the " + + "security considerations covered in that method's JavaDoc before doing so). Header: "; + + private static final String JWE_NONE_MSG = "JWEs do not support key management " + AbstractHeader.ALGORITHM + + " header value '" + Jwts.SIG.NONE.getId() + "' per " + + "https://www.rfc-editor.org/rfc/rfc7518.html#section-4.1"; + + private static final String JWS_NONE_SIG_MISMATCH_MSG = "The JWS header references signature algorithm '" + + Jwts.SIG.NONE.getId() + "' yet the compact JWS string contains a signature. This is not permitted " + + "per https://tools.ietf.org/html/rfc7518#section-3.6."; + private static final String UNPROTECTED_DECOMPRESSION_MSG = "The JWT header references compression algorithm " + + "'%s', but payload decompression for Unsecured JWTs (those with an " + AbstractHeader.ALGORITHM + + " header value of '" + Jwts.SIG.NONE.getId() + "') are " + "disallowed by default to protect " + + "against [Denial of Service attacks](" + + "https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-pellegrino.pdf). If you " + + "wish to enable Unsecure JWS payload decompression, call the JwtParserBuilder." + + "enableUnsecuredDecompression() method (but please read the security considerations covered in that " + + "method's JavaDoc before doing so)."; + + private static IdRegistry newRegistry(String name, Collection defaults, Collection extras) { + Collection all = new LinkedHashSet<>(Collections.size(extras) + defaults.size()); + all.addAll(extras); + all.addAll(defaults); + return new IdRegistry<>(name, all); + } + + private static Function> sigFn(Collection> extras) { + String name = "JWS MAC or Signature Algorithm"; + IdRegistry> registry = newRegistry(name, Jwts.SIG.values(), extras); + return new IdLocator<>(AbstractHeader.ALGORITHM, MISSING_JWS_ALG_MSG, registry); + } + + private static Function encFn(Collection extras) { + String name = "JWE Encryption Algorithm"; + IdRegistry registry = newRegistry(name, Jwts.ENC.values(), extras); + return new IdLocator<>(DefaultJweHeader.ENCRYPTION_ALGORITHM, MISSING_ENC_MSG, registry); + } + + private static Function> keyFn(Collection> extras) { + String name = "JWE Key Management Algorithm"; + IdRegistry> registry = newRegistry(name, Jwts.KEY.values(), extras); + return new IdLocator<>(AbstractHeader.ALGORITHM, MISSING_JWE_ALG_MSG, registry); + } + + // TODO: make the following fields final for v1.0 + private Provider provider; + + @SuppressWarnings("deprecation") // will remove for 1.0 private SigningKeyResolver signingKeyResolver; - private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); + private Locator compressionCodecLocator; + + private final boolean enableUnsecuredJws; + + private final boolean enableUnsecuredDecompression; + + private final Function> signatureAlgorithmLocator; + + private final Function encryptionAlgorithmLocator; + + private final Function> keyAlgorithmLocator; + + private final Locator keyLocator; private Decoder base64UrlDecoder = Decoders.BASE64URL; @@ -82,29 +192,51 @@ public class DefaultJwtParser implements JwtParser { /** * TODO: remove this constructor before 1.0 + * * @deprecated for backward compatibility only, see other constructors. */ + @SuppressWarnings("DeprecatedIsStillUsed") // will remove before 1.0 @Deprecated - public DefaultJwtParser() { } + public DefaultJwtParser() { + this.keyLocator = new ConstantKeyLocator(null, null); + this.signatureAlgorithmLocator = sigFn(Collections.>emptyList()); + this.keyAlgorithmLocator = keyFn(Collections.>emptyList()); + this.encryptionAlgorithmLocator = encFn(Collections.emptyList()); + this.compressionCodecLocator = new DefaultCompressionCodecResolver(); + this.enableUnsecuredJws = false; + this.enableUnsecuredDecompression = false; + } - DefaultJwtParser(SigningKeyResolver signingKeyResolver, - Key key, - byte[] keyBytes, + //SigningKeyResolver will be removed for 1.0: + @SuppressWarnings("deprecation") + DefaultJwtParser(Provider provider, + SigningKeyResolver signingKeyResolver, + boolean enableUnsecuredJws, + boolean enableUnsecuredDecompression, + Locator keyLocator, Clock clock, long allowedClockSkewMillis, Claims expectedClaims, Decoder base64UrlDecoder, Deserializer> deserializer, - CompressionCodecResolver compressionCodecResolver) { + Locator compressionCodecLocator, + Collection> extraSigAlgs, + Collection> extraKeyAlgs, + Collection extraEncAlgs) { + this.provider = provider; + this.enableUnsecuredJws = enableUnsecuredJws; + this.enableUnsecuredDecompression = enableUnsecuredDecompression; this.signingKeyResolver = signingKeyResolver; - this.key = key; - this.keyBytes = keyBytes; + this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null."); this.clock = clock; this.allowedClockSkewMillis = allowedClockSkewMillis; this.expectedClaims = expectedClaims; this.base64UrlDecoder = base64UrlDecoder; this.deserializer = deserializer; - this.compressionCodecResolver = compressionCodecResolver; + this.signatureAlgorithmLocator = sigFn(extraSigAlgs); + this.keyAlgorithmLocator = keyFn(extraKeyAlgs); + this.encryptionAlgorithmLocator = encFn(extraEncAlgs); + this.compressionCodecLocator = Assert.notNull(compressionCodecLocator, "CompressionCodec locator cannot be null."); } @Override @@ -188,24 +320,24 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; - return this; + return setSigningKey(Keys.hmacShaKeyFor(key)); } @Override public JwtParser setSigningKey(String base64EncodedSecretKey) { Assert.hasText(base64EncodedSecretKey, "signing key cannot be null or empty."); - this.keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); - return this; + byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); + return setSigningKey(bytes); } @Override - public JwtParser setSigningKey(Key key) { + public JwtParser setSigningKey(final Key key) { Assert.notNull(key, "signing key cannot be null."); - this.key = key; + setSigningKeyResolver(new ConstantKeyLocator(key, null)); return this; } + @SuppressWarnings("deprecation") // required until 1.0 @Override public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); @@ -213,211 +345,351 @@ public class DefaultJwtParser implements JwtParser { return this; } + @SuppressWarnings("deprecation") @Override public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); - this.compressionCodecResolver = compressionCodecResolver; + this.compressionCodecLocator = new CompressionCodecLocator(compressionCodecResolver); return this; } @Override - public boolean isSigned(String jwt) { + public boolean isSigned(String compact) { + if (compact == null) { + return false; + } + try { + final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact); + return !(tokenized instanceof TokenizedJwe) && Strings.hasText(tokenized.getDigest()); + } catch (MalformedJwtException e) { + return false; + } + } - if (jwt == null) { + private static boolean hasContentType(Header header) { + return header != null && Strings.hasText(header.getContentType()); + } + + + /** + * Returns {@code true} IFF the specified payload starts with a { character and ends with a + * } 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 not delegated to a JSON Deserializer. + * + * @param payload the byte array that could be JSON + * @return {@code true} IFF the specified payload starts with a { character and ends with a + * } 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 delimiterCount = 0; + 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 < jwt.length(); i++) { - char c = jwt.charAt(i); - - if (delimiterCount == 2) { - return !Character.isWhitespace(c) && c != SEPARATOR_CHAR; + for (int i = 0; i < len; i++) { + int c = payload[i]; + if (c == '{') { + jsonStartIndex = i; + break; } - - if (c == SEPARATOR_CHAR) { - delimiterCount++; + } + 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; } } - return false; + for (int i = maxIndex; i > jsonStartIndex; i--) { + int c = payload[i]; + if (c == '}') { + jsonEndIndex = i; + break; + } + } + + if (jsonEndIndex > maxIndex) { // found start '{' char, but no closing '} char. Can't be a JSON object + return false; + } + + if (jsonEndIndex < maxIndex) { + // We found content at the end of the payload, after the last '}' character. We need to check to see if + // any of it (when UTF-8 decoded) is not whitespace. If so, it's not a valid JSON object payload. + int size = maxIndex - jsonEndIndex; + byte[] trailing = new byte[size]; + System.arraycopy(payload, jsonEndIndex + 1, trailing, 0, size); + String s = new String(trailing, StandardCharsets.UTF_8); + return !Strings.hasText(s); // if just whitespace after last '}', we can assume JSON and try and parse it + } + + return true; + } + + private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHeader, final String alg, + @SuppressWarnings("deprecation") SigningKeyResolver resolver, + Claims claims, byte[] payload) { + + Assert.notNull(resolver, "SigningKeyResolver instance cannot be null."); + + SecureDigestAlgorithm algorithm; + try { + algorithm = (SecureDigestAlgorithm) signatureAlgorithmLocator.apply(jwsHeader); + } catch (UnsupportedJwtException e) { + //For backwards compatibility. TODO: remove this try/catch block for 1.0 and let UnsupportedJwtException propagate + String msg = "Unsupported signature algorithm '" + alg + "'"; + throw new SignatureException(msg, e); + } + Assert.stateNotNull(algorithm, "JWS Signature Algorithm cannot be null."); + + //digitally signed, let's assert the signature: + Key key; + if (claims != null) { + key = resolver.resolveSigningKey(jwsHeader, claims); + } else { + key = resolver.resolveSigningKey(jwsHeader, payload); + } + if (key == null) { + String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader; + throw new UnsupportedJwtException(msg); + } + + //re-create the jwt part without the signature. This is what is needed for signature verification: + String jwtWithoutSignature = tokenized.getProtected() + SEPARATOR_CHAR + tokenized.getBody(); + + byte[] data = jwtWithoutSignature.getBytes(StandardCharsets.US_ASCII); + byte[] signature = base64UrlDecode(tokenized.getDigest(), "JWS signature"); + + try { + VerifySecureDigestRequest request = + new DefaultVerifySecureDigestRequest<>(data, this.provider, null, key, signature); + if (!algorithm.verify(request)) { + String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + + "asserted and should not be trusted."; + throw new SignatureException(msg); + } + } catch (WeakKeyException e) { + throw e; + } catch (InvalidKeyException | IllegalArgumentException e) { + String algId = algorithm.getId(); + String msg = "The parsed JWT indicates it was signed with the '" + algId + "' signature " + + "algorithm, but the provided " + key.getClass().getName() + " key may " + + "not be used to verify " + algId + " signatures. Because the specified " + + "key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was provided the incorrect " + + "signature verification key, but this cannot be assumed for security reasons."; + throw new UnsupportedJwtException(msg, e); + } } @Override - public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException { + public Jwt parse(String compact) throws ExpiredJwtException, MalformedJwtException, SignatureException { // TODO, this logic is only need for a now deprecated code path // remove this block in v1.0 (the equivalent is already in DefaultJwtParserBuilder) if (this.deserializer == null) { // try to find one based on the services available // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 - this.deserializeJsonWith(LegacyServices.loadFirst(Deserializer.class)); + //noinspection deprecation + this.deserializer = LegacyServices.loadFirst(Deserializer.class); } - Assert.hasText(jwt, "JWT String argument cannot be null or empty."); + Assert.hasText(compact, "JWT String cannot be null or empty."); - if ("..".equals(jwt)) { - String msg = "JWT string '..' is missing a header."; + final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact); + final String base64UrlHeader = tokenized.getProtected(); + if (!Strings.hasText(base64UrlHeader)) { + String msg = "Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4)."; throw new MalformedJwtException(msg); } - String base64UrlEncodedHeader = null; - String base64UrlEncodedPayload = null; - String base64UrlEncodedDigest = null; - - int delimiterCount = 0; - - StringBuilder sb = new StringBuilder(128); - - for (char c : jwt.toCharArray()) { - - if (c == SEPARATOR_CHAR) { - - CharSequence tokenSeq = Strings.clean(sb); - String token = tokenSeq != null ? tokenSeq.toString() : null; - - if (delimiterCount == 0) { - base64UrlEncodedHeader = token; - } else if (delimiterCount == 1) { - base64UrlEncodedPayload = token; - } - - delimiterCount++; - sb.setLength(0); - } else { - sb.append(c); - } - } - - if (delimiterCount != 2) { - String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount; - throw new MalformedJwtException(msg); - } - if (sb.length() > 0) { - base64UrlEncodedDigest = sb.toString(); - } - - // =============== Header ================= - Header header = null; + final byte[] headerBytes = base64UrlDecode(base64UrlHeader, "protected header"); + Map m = readValue(headerBytes, "protected header"); + Header header; + try { + header = tokenized.createHeader(m); + } catch (Exception e) { + String msg = "Invalid protected header: " + e.getMessage(); + throw new MalformedJwtException(msg, e); + } - CompressionCodec compressionCodec = null; + // https://tools.ietf.org/html/rfc7515#section-10.7 , second-to-last bullet point, note the use of 'always': + // + // * Require that the "alg" Header Parameter be carried in the JWS + // Protected Header. (This is always the case when using the JWS + // Compact Serialization and is the approach taken by CMS [RFC6211].) + // + final String alg = Strings.clean(header.getAlgorithm()); + if (!Strings.hasText(alg)) { + String msg = tokenized instanceof TokenizedJwe ? MISSING_JWE_ALG_MSG : MISSING_JWS_ALG_MSG; + throw new MalformedJwtException(msg); + } + final boolean unsecured = Jwts.SIG.NONE.getId().equalsIgnoreCase(alg); - if (base64UrlEncodedHeader != null) { - byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedHeader); - String origValue = new String(bytes, Strings.UTF_8); - Map m = (Map) readValue(origValue); - - if (base64UrlEncodedDigest != null) { - header = new DefaultJwsHeader(m); - } else { - header = new DefaultHeader(m); + final String base64UrlDigest = tokenized.getDigest(); + final boolean hasDigest = Strings.hasText(base64UrlDigest); + if (unsecured) { + if (tokenized instanceof TokenizedJwe) { + throw new MalformedJwtException(JWE_NONE_MSG); } - - compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); + // Unsecured JWTs are disabled by default per the RFC: + if (!enableUnsecuredJws) { + String msg = UNSECURED_DISABLED_MSG_PREFIX + header; + throw new UnsupportedJwtException(msg); + } + if (hasDigest) { + throw new MalformedJwtException(JWS_NONE_SIG_MISMATCH_MSG); + } + } else if (!hasDigest) { // something other than 'none'. Must have a digest component: + String fmt = tokenized instanceof TokenizedJwe ? MISSING_JWE_DIGEST_MSG_FMT : MISSING_JWS_DIGEST_MSG_FMT; + String msg = String.format(fmt, alg); + throw new MalformedJwtException(msg); } // =============== Body ================= - String payload = ""; // https://github.com/jwtk/jjwt/pull/540 - if (base64UrlEncodedPayload != null) { - byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload); - if (compressionCodec != null) { - bytes = compressionCodec.decompress(bytes); - } - payload = new String(bytes, Strings.UTF_8); + byte[] payload = base64UrlDecode(tokenized.getBody(), "payload"); + if (tokenized instanceof TokenizedJwe && Arrays.length(payload) == 0) { // Only JWS body can be empty per https://github.com/jwtk/jjwt/pull/540 + String msg = "Compact JWE strings MUST always contain a payload (ciphertext)."; + throw new MalformedJwtException(msg); } - Claims claims = null; + byte[] iv = null; + byte[] tag = null; + if (tokenized instanceof TokenizedJwe) { - if (!payload.isEmpty() && payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: - Map claimsMap = (Map) readValue(payload); - claims = new DefaultClaims(claimsMap); - } + TokenizedJwe tokenizedJwe = (TokenizedJwe) tokenized; + JweHeader jweHeader = (JweHeader) header; - // =============== Signature ================= - if (base64UrlEncodedDigest != null) { //it is signed - validate the signature - - JwsHeader jwsHeader = (JwsHeader) header; - - SignatureAlgorithm algorithm = null; - - if (header != null) { - String alg = jwsHeader.getAlgorithm(); - if (Strings.hasText(alg)) { - algorithm = SignatureAlgorithm.forName(alg); + byte[] cekBytes = Bytes.EMPTY; //ignored unless using an encrypted key algorithm + String base64Url = tokenizedJwe.getEncryptedKey(); + if (Strings.hasText(base64Url)) { + cekBytes = base64UrlDecode(base64Url, "JWE encrypted key"); + if (Arrays.length(cekBytes) == 0) { + String msg = "Compact JWE string represents an encrypted key, but the key is empty."; + throw new MalformedJwtException(msg); } } - if (algorithm == null || algorithm == SignatureAlgorithm.NONE) { - //it is plaintext, but it has a signature. This is invalid: - String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + - "algorithm."; + base64Url = tokenizedJwe.getIv(); + if (Strings.hasText(base64Url)) { + iv = base64UrlDecode(base64Url, "JWE Initialization Vector"); + } + if (Arrays.length(iv) == 0) { + String msg = "Compact JWE strings must always contain an Initialization Vector."; throw new MalformedJwtException(msg); } - if (key != null && keyBytes != null) { - throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); - } else if ((key != null || keyBytes != null) && signingKeyResolver != null) { - String object = key != null ? "a key object" : "key bytes"; - throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either."); + // The AAD (Additional Authenticated Data) scheme for compact JWEs is to use the ASCII bytes of the + // raw base64url text as the AAD, and NOT the base64url-decoded bytes per + // https://www.rfc-editor.org/rfc/rfc7516.html#section-5.1, Step 14. + final byte[] aad = base64UrlHeader.getBytes(StandardCharsets.US_ASCII); + + base64Url = base64UrlDigest; + //guaranteed to be non-empty via the `alg` + digest check above: + Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty."); + tag = base64UrlDecode(base64Url, "JWE AAD Authentication Tag"); + if (Arrays.length(tag) == 0) { + String msg = "Compact JWE strings must always contain an AAD Authentication Tag."; + throw new MalformedJwtException(msg); } - //digitally signed, let's assert the signature: - Key key = this.key; + String enc = jweHeader.getEncryptionAlgorithm(); + if (!Strings.hasText(enc)) { + throw new MalformedJwtException(MISSING_ENC_MSG); + } + final AeadAlgorithm encAlg = this.encryptionAlgorithmLocator.apply(jweHeader); + Assert.stateNotNull(encAlg, "JWE Encryption Algorithm cannot be null."); - if (key == null) { //fall back to keyBytes + @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgorithmLocator.apply(jweHeader); + Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null."); - byte[] keyBytes = this.keyBytes; - - if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver - if (claims != null) { - key = signingKeyResolver.resolveSigningKey(jwsHeader, claims); - } else { - key = signingKeyResolver.resolveSigningKey(jwsHeader, payload); - } - } - - if (!Objects.isEmpty(keyBytes)) { - - Assert.isTrue(algorithm.isHmac(), - "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); - - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); - } + final Key key = this.keyLocator.locate(jweHeader); + if (key == null) { + String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader; + throw new UnsupportedJwtException(msg); } - Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); - - //re-create the jwt part without the signature. This is what needs to be signed for verification: - String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR; - if (base64UrlEncodedPayload != null) { - jwtWithoutSignature += base64UrlEncodedPayload; + DecryptionKeyRequest request = + new DefaultDecryptionKeyRequest<>(cekBytes, this.provider, null, jweHeader, encAlg, key); + final SecretKey cek = keyAlg.getDecryptionKey(request); + if (cek == null) { + String msg = "The '" + keyAlg.getId() + "' JWE key algorithm did not return a decryption key. " + + "Unable to perform '" + encAlg.getId() + "' decryption."; + throw new IllegalStateException(msg); } - JwtSignatureValidator validator; + DecryptAeadRequest decryptRequest = + new DefaultAeadResult(this.provider, null, payload, cek, aad, tag, iv); + Message result = encAlg.decrypt(decryptRequest); + payload = result.getPayload(); + + } else if (hasDigest && this.signingKeyResolver == null) { //TODO: for 1.0, remove the == null check + // not using a signing key resolver, so we can verify the signature before reading the body, which is + // always safer: + verifySignature(tokenized, ((JwsHeader) header), alg, new LocatingKeyResolver(this.keyLocator), null, null); + } + + CompressionCodec compressionCodec = compressionCodecLocator.locate(header); + if (compressionCodec != null) { + if (unsecured && !enableUnsecuredDecompression) { + String msg = String.format(UNPROTECTED_DECOMPRESSION_MSG, compressionCodec.getId()); + throw new UnsupportedJwtException(msg); + } + payload = compressionCodec.decompress(payload); + } + + Claims claims = null; + if (!hasContentType(header) // If there is a content type set, then the application using JJWT is expected + // to convert the byte payload themselves based on this content type + // https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + // + // "This parameter is ignored by JWS implementations; any processing of this + // parameter is performed by the JWS application." + // + && isLikelyJson(payload)) { // likely to be json, parse it: + Map claimsMap = readValue(payload, "claims"); try { - algorithm.assertValidVerificationKey(key); //since 0.10.0: https://github.com/jwtk/jjwt/issues/334 - validator = createSignatureValidator(algorithm, key); - } catch (WeakKeyException e) { - throw e; - } catch (InvalidKeyException | IllegalArgumentException e) { - String algName = algorithm.getValue(); - String msg = "The parsed JWT indicates it was signed with the '" + algName + "' signature " + - "algorithm, but the provided " + key.getClass().getName() + " key may " + - "not be used to verify " + algName + " signatures. Because the specified " + - "key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was provided the incorrect " + - "signature verification key, but this cannot be assumed for security reasons."; - throw new UnsupportedJwtException(msg, e); + claims = new DefaultClaims(claimsMap); + } catch (Exception e) { + String msg = "Invalid claims: " + e.getMessage(); + throw new MalformedJwtException(msg, e); } + } - if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { - String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + - "asserted and should not be trusted."; - throw new SignatureException(msg); - } + Jwt jwt; + Object body = claims != null ? claims : payload; + if (header instanceof JweHeader) { + jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag); + } else if (hasDigest) { + JwsHeader jwsHeader = Assert.isInstanceOf(JwsHeader.class, header, "JwsHeader required."); + jwt = new DefaultJws<>(jwsHeader, body, base64UrlDigest); + } else { + //noinspection rawtypes + jwt = new DefaultJwt(header, body); + } + + // =============== Signature ================= + if (hasDigest && signingKeyResolver != null) { // TODO: remove for 1.0 + // A SigningKeyResolver has been configured, and due to it's API, we have to verify the signature after + // parsing the body. This can be a security risk, so it needs to be removed before 1.0 + verifySignature(tokenized, ((JwsHeader) header), alg, this.signingKeyResolver, claims, payload); } final boolean allowSkew = this.allowedClockSkewMillis > 0; @@ -439,11 +711,11 @@ public class DefaultJwtParser implements JwtParser { String expVal = DateFormats.formatIso8601(exp, false); String nowVal = DateFormats.formatIso8601(now, false); - long differenceMillis = maxTime - exp.getTime(); + long differenceMillis = nowTime - exp.getTime(); String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + - differenceMillis + " milliseconds. Allowed clock skew: " + - this.allowedClockSkewMillis + " milliseconds."; + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; throw new ExpiredJwtException(header, claims, msg); } } @@ -462,9 +734,9 @@ public class DefaultJwtParser implements JwtParser { long differenceMillis = nbf.getTime() - minTime; String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + - ", a difference of " + - differenceMillis + " milliseconds. Allowed clock skew: " + - this.allowedClockSkewMillis + " milliseconds."; + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; throw new PrematureJwtException(header, claims, msg); } } @@ -472,13 +744,7 @@ public class DefaultJwtParser implements JwtParser { validateExpectedClaims(header, claims); } - Object body = claims != null ? claims : payload; - - if (base64UrlEncodedDigest != null) { - return new DefaultJws<>((JwsHeader) header, body, base64UrlEncodedDigest); - } else { - return new DefaultJwt<>(header, body); - } + return jwt; } /** @@ -491,7 +757,7 @@ public class DefaultJwtParser implements JwtParser { return o; } - private void validateExpectedClaims(Header header, Claims claims) { + private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { @@ -503,110 +769,90 @@ public class DefaultJwtParser implements JwtParser { actualClaimValue = claims.get(expectedClaimName, Date.class); } catch (Exception e) { String msg = "JWT Claim '" + expectedClaimName + "' was expected to be a Date, but its value " + - "cannot be converted to a Date using current heuristics. Value: " + actualClaimValue; - throw new IncorrectClaimException(header, claims, msg); + "cannot be converted to a Date using current heuristics. Value: " + actualClaimValue; + throw new IncorrectClaimException(header, claims, expectedClaimName, expectedClaimValue, msg); } } - InvalidClaimException invalidClaimException = null; - if (actualClaimValue == null) { - - String msg = String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue); - - invalidClaimException = new MissingClaimException(header, claims, msg); - + String msg = String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue); + throw new MissingClaimException(header, claims, expectedClaimName, expectedClaimValue, msg); } else if (!expectedClaimValue.equals(actualClaimValue)) { - - String msg = String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue, actualClaimValue); - - invalidClaimException = new IncorrectClaimException(header, claims, msg); - } - - if (invalidClaimException != null) { - invalidClaimException.setClaimName(expectedClaimName); - invalidClaimException.setClaimValue(expectedClaimValue); - throw invalidClaimException; + String msg = String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue, actualClaimValue); + throw new IncorrectClaimException(header, claims, expectedClaimName, expectedClaimValue, msg); } } } - /* - * @since 0.5 mostly to allow testing overrides - */ - protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSignatureValidator(alg, key, base64UrlDecoder); - } - @Override public T parse(String compact, JwtHandler handler) - throws ExpiredJwtException, MalformedJwtException, SignatureException { + throws ExpiredJwtException, MalformedJwtException, SignatureException { Assert.notNull(handler, "JwtHandler argument cannot be null."); Assert.hasText(compact, "JWT String argument cannot be null or empty."); - Jwt jwt = parse(compact); + Jwt jwt = parse(compact); if (jwt instanceof Jws) { - Jws jws = (Jws) jwt; - Object body = jws.getBody(); + Jws jws = (Jws) jwt; + Object body = jws.getPayload(); if (body instanceof Claims) { return handler.onClaimsJws((Jws) jws); } else { - return handler.onPlaintextJws((Jws) jws); + return handler.onContentJws((Jws) jws); + } + } else if (jwt instanceof Jwe) { + Jwe jwe = (Jwe) jwt; + Object body = jwe.getPayload(); + if (body instanceof Claims) { + return handler.onClaimsJwe((Jwe) jwe); + } else { + return handler.onContentJwe((Jwe) jwe); } } else { - Object body = jwt.getBody(); + Object body = jwt.getPayload(); if (body instanceof Claims) { - return handler.onClaimsJwt((Jwt) jwt); + return handler.onClaimsJwt((Jwt) jwt); } else { - return handler.onPlaintextJwt((Jwt) jwt); + return handler.onContentJwt((Jwt) jwt); } } } @Override - public Jwt parsePlaintextJwt(String plaintextJwt) { - return parse(plaintextJwt, new JwtHandlerAdapter>() { + public Jwt parseContentJwt(String compact) { + return parse(compact, new JwtHandlerAdapter>() { @Override - public Jwt onPlaintextJwt(Jwt jwt) { + public Jwt onContentJwt(Jwt jwt) { return jwt; } }); } @Override - public Jwt parseClaimsJwt(String claimsJwt) { - try { - return parse(claimsJwt, new JwtHandlerAdapter>() { - @Override - public Jwt onClaimsJwt(Jwt jwt) { - return jwt; - } - }); - } catch (IllegalArgumentException iae) { - throw new UnsupportedJwtException("Signed JWSs are not supported.", iae); - } + public Jwt parseClaimsJwt(String compact) { + return parse(compact, new JwtHandlerAdapter>() { + @Override + public Jwt onClaimsJwt(Jwt jwt) { + return jwt; + } + }); } @Override - public Jws parsePlaintextJws(String plaintextJws) { - try { - return parse(plaintextJws, new JwtHandlerAdapter>() { - @Override - public Jws onPlaintextJws(Jws jws) { - return jws; - } - }); - } catch (IllegalArgumentException iae) { - throw new UnsupportedJwtException("Signed JWSs are not supported.", iae); - } + public Jws parseContentJws(String compact) { + return parse(compact, new JwtHandlerAdapter>() { + @Override + public Jws onContentJws(Jws jws) { + return jws; + } + }); } @Override - public Jws parseClaimsJws(String claimsJws) { - return parse(claimsJws, new JwtHandlerAdapter>() { + public Jws parseClaimsJws(String compact) { + return parse(compact, new JwtHandlerAdapter>() { @Override public Jws onClaimsJws(Jws jws) { return jws; @@ -614,9 +860,41 @@ public class DefaultJwtParser implements JwtParser { }); } - @SuppressWarnings("unchecked") - protected Map readValue(String val) { - byte[] bytes = val.getBytes(Strings.UTF_8); - return deserializer.deserialize(bytes); + @Override + public Jwe parseContentJwe(String compact) throws JwtException { + return parse(compact, new JwtHandlerAdapter>() { + @Override + public Jwe onContentJwe(Jwe jwe) { + return jwe; + } + }); + } + + @Override + public Jwe parseClaimsJwe(String compact) throws JwtException { + return parse(compact, new JwtHandlerAdapter>() { + @Override + public Jwe onClaimsJwe(Jwe jwe) { + return jwe; + } + }); + } + + protected byte[] base64UrlDecode(String base64UrlEncoded, String name) { + try { + return base64UrlDecoder.decode(base64UrlEncoded); + } catch (DecodingException e) { + String msg = "Invalid Base64Url " + name + ": " + base64UrlEncoded; + throw new MalformedJwtException(msg, e); + } + } + + protected Map readValue(byte[] bytes, final String name) { + try { + return deserializer.deserialize(bytes); + } catch (MalformedJwtException | DeserializationException e) { + String s = new String(bytes, StandardCharsets.UTF_8); + throw new MalformedJwtException("Unable to read " + name + " JSON: " + s, e); + } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 3b299ebf..93e35838 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -17,19 +17,30 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; +import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Locator; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; +import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.impl.security.ConstantKeyLocator; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SecureDigestAlgorithm; import java.security.Key; +import java.security.Provider; +import java.util.Collection; import java.util.Date; +import java.util.LinkedHashSet; import java.util.Map; /** @@ -41,32 +52,65 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { /** * To prevent overflow per Issue 583. - * + *

* Package-protected on purpose to allow use in backwards-compatible {@link DefaultJwtParser} implementation. * TODO: enable private modifier on these two variables when deleting DefaultJwtParser */ static final long MAX_CLOCK_SKEW_MILLIS = Long.MAX_VALUE / MILLISECONDS_PER_SECOND; static final String MAX_CLOCK_SKEW_ILLEGAL_MSG = "Illegal allowedClockSkewMillis value: multiplying this " + - "value by 1000 to obtain the number of milliseconds would cause a numeric overflow."; + "value by 1000 to obtain the number of milliseconds would cause a numeric overflow."; - private byte[] keyBytes; + private Provider provider; - private Key key; + private boolean enableUnsecuredJws = false; - private SigningKeyResolver signingKeyResolver; + private boolean enableUnsecuredDecompression = false; - private CompressionCodecResolver compressionCodecResolver; + private Locator keyLocator; + + @SuppressWarnings("deprecation") //TODO: remove for 1.0 + private SigningKeyResolver signingKeyResolver = null; + + private Locator compressionCodecLocator; + + private final Collection extraEncryptionAlgorithms = new LinkedHashSet<>(); + + private final Collection> extraKeyAlgorithms = new LinkedHashSet<>(); + + private final Collection> extraDigestAlgorithms = new LinkedHashSet<>(); + + private final Collection extraCompressionCodecs = new LinkedHashSet<>(); private Decoder base64UrlDecoder = Decoders.BASE64URL; private Deserializer> deserializer; - private Claims expectedClaims = new DefaultClaims(); + private final Claims expectedClaims = new DefaultClaims(); private Clock clock = DefaultClock.INSTANCE; private long allowedClockSkewMillis = 0; + private Key signatureVerificationKey; + private Key decryptionKey; + + @Override + public JwtParserBuilder enableUnsecuredJws() { + this.enableUnsecuredJws = true; + return this; + } + + @Override + public JwtParserBuilder enableUnsecuredDecompression() { + this.enableUnsecuredDecompression = true; + return this; + } + + @Override + public JwtParserBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } @Override public JwtParserBuilder deserializeJsonWith(Deserializer> deserializer) { @@ -148,25 +192,63 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { @Override public JwtParserBuilder setSigningKey(byte[] key) { - Assert.notEmpty(key, "signing key cannot be null or empty."); - this.keyBytes = key; - return this; + Assert.notEmpty(key, "signature verification key cannot be null or empty."); + return setSigningKey(Keys.hmacShaKeyFor(key)); } @Override public JwtParserBuilder setSigningKey(String base64EncodedSecretKey) { - Assert.hasText(base64EncodedSecretKey, "signing key cannot be null or empty."); - this.keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); + Assert.hasText(base64EncodedSecretKey, "signature verification key cannot be null or empty."); + byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey); + return setSigningKey(bytes); + } + + @Override + public JwtParserBuilder setSigningKey(final Key key) { + return verifyWith(key); + } + + @Override + public JwtParserBuilder verifyWith(Key key) { + this.signatureVerificationKey = Assert.notNull(key, "signature verification key cannot be null."); return this; } @Override - public JwtParserBuilder setSigningKey(Key key) { - Assert.notNull(key, "signing key cannot be null."); - this.key = key; + public JwtParserBuilder decryptWith(final Key key) { + this.decryptionKey = Assert.notNull(key, "decryption key cannot be null."); return this; } + @Override + public JwtParserBuilder addCompressionCodecs(Collection codecs) { + Assert.notEmpty(codecs, "Additional CompressionCodec collection cannot be null or empty."); + this.extraCompressionCodecs.addAll(codecs); + return this; + } + + @Override + public JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs) { + Assert.notEmpty(encAlgs, "Additional AeadAlgorithm collection cannot be null or empty."); + this.extraEncryptionAlgorithms.addAll(encAlgs); + return this; + } + + @Override + public JwtParserBuilder addSignatureAlgorithms(Collection> sigAlgs) { + Assert.notEmpty(sigAlgs, "Additional SignatureAlgorithm collection cannot be null or empty."); + this.extraDigestAlgorithms.addAll(sigAlgs); + return this; + } + + @Override + public JwtParserBuilder addKeyAlgorithms(Collection> keyAlgs) { + Assert.notEmpty(keyAlgs, "Additional KeyAlgorithm collection cannot be null or empty."); + this.extraKeyAlgorithms.addAll(keyAlgs); + return this; + } + + @SuppressWarnings("deprecation") //TODO: remove for 1.0 @Override public JwtParserBuilder setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null."); @@ -175,9 +257,21 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { } @Override - public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { - Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); - this.compressionCodecResolver = compressionCodecResolver; + public JwtParserBuilder setKeyLocator(Locator keyLocator) { + this.keyLocator = Assert.notNull(keyLocator, "Key locator cannot be null."); + return this; + } + + @Override + public JwtParserBuilder setCompressionCodecLocator(Locator locator) { + this.compressionCodecLocator = Assert.notNull(locator, "CompressionCodec locator cannot be null."); + return this; + } + + @Override + public JwtParserBuilder setCompressionCodecResolver(CompressionCodecResolver resolver) { + Assert.notNull(resolver, "compressionCodecResolver cannot be null."); + this.compressionCodecLocator = new CompressionCodecLocator(resolver); return this; } @@ -188,23 +282,58 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { // that is NOT exposed as a service and no other implementations are available for lookup. if (this.deserializer == null) { // try to find one based on the services available: + //noinspection unchecked this.deserializer = Services.loadFirst(Deserializer.class); } - // if the compressionCodecResolver is not set default it. - if (this.compressionCodecResolver == null) { - this.compressionCodecResolver = new DefaultCompressionCodecResolver(); + if (this.signingKeyResolver != null && this.signatureVerificationKey != null) { + String msg = "Both 'signingKeyResolver and 'verifyWith/signWith' key cannot be configured. " + + "Choose either, or prefer `keyLocator` when possible."; + throw new IllegalStateException(msg); + } + if (this.keyLocator != null && this.decryptionKey != null) { + String msg = "Both 'keyLocator' and 'decryptWith' key cannot be configured. Prefer 'keyLocator' if possible."; + throw new IllegalStateException(msg); + } + if (this.keyLocator == null) { + this.keyLocator = new ConstantKeyLocator(this.signatureVerificationKey, this.decryptionKey); } - return new ImmutableJwtParser( - new DefaultJwtParser(signingKeyResolver, - key, - keyBytes, - clock, - allowedClockSkewMillis, - expectedClaims, - base64UrlDecoder, - new JwtDeserializer<>(deserializer), - compressionCodecResolver)); + if (!enableUnsecuredJws && enableUnsecuredDecompression) { + String msg = "'enableUnsecuredDecompression' is only relevant if 'enableUnsecuredJws' is also " + + "configured. Please read the JavaDoc of both features before enabling either " + + "due to their security implications."; + throw new IllegalStateException(msg); + } + if (this.compressionCodecLocator != null && !Collections.isEmpty(extraCompressionCodecs)) { + String msg = "Both 'addCompressionCodecs' and 'compressionCodecLocator' " + + "(or 'compressionCodecResolver') cannot be specified. Choose either."; + throw new IllegalStateException(msg); + } + if (this.compressionCodecLocator == null) { + this.compressionCodecLocator = new DefaultCompressionCodecResolver(extraCompressionCodecs); + } + + // Invariants. If these are ever violated, it's an error in this class implementation + // (we default to non-null instances, and the setters should never allow null): + Assert.stateNotNull(this.keyLocator, "Key locator should never be null."); + Assert.stateNotNull(this.compressionCodecLocator, "CompressionCodec Locator should never be null."); + + return new ImmutableJwtParser(new DefaultJwtParser( + provider, + signingKeyResolver, + enableUnsecuredJws, + enableUnsecuredDecompression, + keyLocator, + clock, + allowedClockSkewMillis, + expectedClaims, + base64UrlDecoder, + new JwtDeserializer<>(deserializer), + compressionCodecLocator, + extraDigestAlgorithms, + extraKeyAlgorithms, + extraEncryptionAlgorithms + )); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java new file mode 100644 index 00000000..ff897526 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwe.java @@ -0,0 +1,47 @@ +/* + * 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.Header; + +import java.util.Map; + +class DefaultTokenizedJwe extends DefaultTokenizedJwt implements TokenizedJwe { + + private final String encryptedKey; + private final String iv; + + DefaultTokenizedJwe(String protectedHeader, String body, String digest, String encryptedKey, String iv) { + super(protectedHeader, body, digest); + this.encryptedKey = encryptedKey; + this.iv = iv; + } + + @Override + public String getEncryptedKey() { + return this.encryptedKey; + } + + @Override + public String getIv() { + return this.iv; + } + + @Override + public Header createHeader(Map m) { + return new DefaultJweHeader(m); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java new file mode 100644 index 00000000..e69cb1dc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultTokenizedJwt.java @@ -0,0 +1,58 @@ +/* + * 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.Header; +import io.jsonwebtoken.lang.Strings; + +import java.util.Map; + +class DefaultTokenizedJwt implements TokenizedJwt { + + private final String protectedHeader; + private final String body; + private final String digest; + + DefaultTokenizedJwt(String protectedHeader, String body, String digest) { + this.protectedHeader = protectedHeader; + this.body = body; + this.digest = digest; + } + + @Override + public String getProtected() { + return this.protectedHeader; + } + + @Override + public String getBody() { + return this.body; + } + + @Override + public String getDigest() { + return this.digest; + } + + @Override + public Header createHeader(Map m) { + if (Strings.hasText(getDigest())) { + return new DefaultJwsHeader(m); + } + return new DefaultUnprotectedHeader(m); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultUnprotectedHeader.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultUnprotectedHeader.java new file mode 100644 index 00000000..6dc78bd7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultUnprotectedHeader.java @@ -0,0 +1,31 @@ +/* + * 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.UnprotectedHeader; + +import java.util.Map; + +public class DefaultUnprotectedHeader extends AbstractHeader implements UnprotectedHeader { + + public DefaultUnprotectedHeader() { + super(AbstractHeader.FIELDS); + } + + public DefaultUnprotectedHeader(Map values) { + super(AbstractHeader.FIELDS, values); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultUnprotectedHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultUnprotectedHeaderBuilder.java new file mode 100644 index 00000000..accf9f0a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultUnprotectedHeaderBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.UnprotectedHeader; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultUnprotectedHeaderBuilder extends AbstractHeaderBuilder + implements UnprotectedHeaderBuilder { + + @Override + protected UnprotectedHeader newHeader() { + return new DefaultUnprotectedHeader(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java b/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java index 205035ce..cecae75e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java @@ -47,6 +47,16 @@ public class FixedClock implements Clock { this.now = now; } + /** + * Creates a new fixed clock using the specified seed timestamp. All calls to + * {@link #now now()} will always return this seed Date. + * + * @param timeInMillis the specified Date in milliseconds to always return from all calls to {@link #now now()}. + */ + public FixedClock(long timeInMillis) { + this(new Date(timeInMillis)); + } + @Override public Date now() { return this.now; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/HeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/HeaderBuilder.java new file mode 100644 index 00000000..a1c4f225 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/HeaderBuilder.java @@ -0,0 +1,25 @@ +/* + * 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.Header; +import io.jsonwebtoken.HeaderMutator; +import io.jsonwebtoken.lang.Builder; +import io.jsonwebtoken.lang.MapMutator; + +// TODO: move this concept to the API when Java 8 is supported. Do we even need it? +public interface HeaderBuilder, T extends HeaderBuilder> extends Builder, MapMutator, HeaderMutator { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java new file mode 100644 index 00000000..d96c4ebb --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java @@ -0,0 +1,74 @@ +/* + * 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.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +public class IdLocator, R> implements Function { + + private final Field headerField; + private final String requiredMsg; + private final boolean headerValueRequired; + + private final Function registry; + + public IdLocator(Field field, String requiredMsg, Function registry) { + this.headerField = Assert.notNull(field, "Header field cannot be null."); + this.registry = Assert.notNull(registry, "Registry cannot be null."); + this.headerValueRequired = Strings.hasText(requiredMsg); + this.requiredMsg = requiredMsg; + } + + private static String type(Header header) { + if (header instanceof JweHeader) { + return "JWE"; + } else if (header instanceof JwsHeader) { + return "JWS"; + } else { + return "JWT"; + } + } + + @Override + public R apply(H header) { + + Assert.notNull(header, "Header argument cannot be null."); + + Object val = header.get(this.headerField.getId()); + String id = val != null ? String.valueOf(val) : null; + + if (this.headerValueRequired && !Strings.hasText(id)) { + throw new MalformedJwtException(requiredMsg); + } + + R instance = registry.apply(id); + + if (this.headerValueRequired && instance == null) { + String msg = "Unrecognized " + type(header) + " " + this.headerField + " header value: " + id; + throw new UnsupportedJwtException(msg); + } + + return instance; + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java index 157864cc..53c5d9a2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/ImmutableJwtParser.java @@ -19,13 +19,15 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwe; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.UnprotectedHeader; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; @@ -139,12 +141,12 @@ class ImmutableJwtParser implements JwtParser { } @Override - public boolean isSigned(String jwt) { - return this.jwtParser.isSigned(jwt); + public boolean isSigned(String compact) { + return this.jwtParser.isSigned(compact); } @Override - public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { return this.jwtParser.parse(jwt); } @@ -154,22 +156,32 @@ class ImmutableJwtParser implements JwtParser { } @Override - public Jwt parsePlaintextJwt(String plaintextJwt) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { - return this.jwtParser.parsePlaintextJwt(plaintextJwt); + public Jwt parseContentJwt(String jwt) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + return this.jwtParser.parseContentJwt(jwt); } @Override - public Jwt parseClaimsJwt(String claimsJwt) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { - return this.jwtParser.parseClaimsJwt(claimsJwt); + public Jwt parseClaimsJwt(String jwt) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + return this.jwtParser.parseClaimsJwt(jwt); } @Override - public Jws parsePlaintextJws(String plaintextJws) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { - return this.jwtParser.parsePlaintextJws(plaintextJws); + public Jws parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + return this.jwtParser.parseContentJws(jws); } @Override - public Jws parseClaimsJws(String claimsJws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { - return this.jwtParser.parseClaimsJws(claimsJws); + public Jws parseClaimsJws(String jws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { + return this.jwtParser.parseClaimsJws(jws); + } + + @Override + public Jwe parseContentJwe(String jwe) throws JwtException { + return this.jwtParser.parseContentJwe(jwe); + } + + @Override + public Jwe parseClaimsJwe(String jwe) throws JwtException { + return this.jwtParser.parseClaimsJwe(jwe); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JweHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/JweHeaderBuilder.java new file mode 100644 index 00000000..1a54871a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/JweHeaderBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.JweHeader; +import io.jsonwebtoken.JweHeaderMutator; + +// TODO: move this concept to the API when Java 8 is supported so we can have JweHeader.builder() --> returns JweHeaderBuilder + +/** + * A builder to create {@link JweHeader} instances. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JweHeaderBuilder extends + ProtectedHeaderBuilder, JweHeaderMutator { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwsHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/JwsHeaderBuilder.java new file mode 100644 index 00000000..94b30fc9 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwsHeaderBuilder.java @@ -0,0 +1,27 @@ +/* + * 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.JwsHeader; + +// TODO: move this concept to the API when Java 8 is supported so we can have JwsHeader.builder() --> returns JwsHeaderBuilder +/** + * A builder to create {@link JwsHeader} instances. + * + * @since JJWT_RELEASE_VERSION + */ +public interface JwsHeaderBuilder extends ProtectedHeaderBuilder { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 99fa6063..465e85fb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -15,161 +15,188 @@ */ package io.jsonwebtoken.impl; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.Nameable; +import io.jsonwebtoken.impl.lang.RedactedSupplier; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.DateFormats; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; -import java.text.ParseException; -import java.util.Calendar; +import java.lang.reflect.Array; import java.util.Collection; -import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; -public class JwtMap implements Map { +public class JwtMap implements Map, FieldReadable, Nameable { - private final Map map; + protected final Map> FIELDS; + protected final Map values; // canonical values formatted per RFC requirements + protected final Map idiomaticValues; // the values map with any RFC values converted to Java type-safe values where possible - public JwtMap() { - this.map = new LinkedHashMap<>(); - } - - public JwtMap(Map map) { - this(); - Assert.notNull(map, "Map argument cannot be null."); - putAll(map); - } - - protected String getString(String name) { - Object v = get(name); - return v != null ? String.valueOf(v) : null; - } - - protected static Date toDate(Object v, String name) { - if (v == null) { - return null; - } else if (v instanceof Date) { - return (Date) v; - } else if (v instanceof Calendar) { //since 0.10.0 - return ((Calendar) v).getTime(); - } else if (v instanceof Number) { - //assume millis: - long millis = ((Number) v).longValue(); - return new Date(millis); - } else if (v instanceof String) { - return parseIso8601Date((String) v, name); //ISO-8601 parsing since 0.10.0 - } else { - throw new IllegalStateException("Cannot create Date from '" + name + "' value '" + v + "'."); + public JwtMap(Set> fieldSet) { + Assert.notEmpty(fieldSet, "Fields cannot be null or empty."); + Map> fields = new LinkedHashMap<>(); + for (Field field : fieldSet) { + fields.put(field.getId(), field); } + this.FIELDS = java.util.Collections.unmodifiableMap(fields); + this.values = new LinkedHashMap<>(); + this.idiomaticValues = new LinkedHashMap<>(); } - // @since 0.10.0 - private static Date parseIso8601Date(String s, String name) throws IllegalArgumentException { - try { - return DateFormats.parseIso8601Date(s); - } catch (ParseException e) { - String msg = "'" + name + "' value does not appear to be ISO-8601-formatted: " + s; - throw new IllegalArgumentException(msg, e); - } + public JwtMap(Set> fieldSet, Map values) { + this(fieldSet); + Assert.notNull(values, "Map argument cannot be null."); + putAll(values); } - // @since 0.10.0 - protected static Date toSpecDate(Object v, String name) { - if (v == null) { - return null; - } else if (v instanceof Number) { - // https://github.com/jwtk/jjwt/issues/122: - // The JWT RFC *mandates* NumericDate values are represented as seconds. - // Because java.util.Date requires milliseconds, we need to multiply by 1000: - long seconds = ((Number) v).longValue(); - v = seconds * 1000; - } else if (v instanceof String) { - // https://github.com/jwtk/jjwt/issues/122 - // The JWT RFC *mandates* NumericDate values are represented as seconds. - // Because java.util.Date requires milliseconds, we need to multiply by 1000: - try { - long seconds = Long.parseLong((String) v); - v = seconds * 1000; - } catch (NumberFormatException ignored) { - } - } - //v would have been normalized to milliseconds if it was a number value, so perform normal date conversion: - return toDate(v, name); + @Override + public String getName() { + return "Map"; } - protected void setValue(String name, Object v) { - if (v == null) { - map.remove(name); - } else { - map.put(name, v); - } + public static boolean isReducibleToNull(Object v) { + return v == null || + (v instanceof String && !Strings.hasText((String) v)) || + (v instanceof Collection && Collections.isEmpty((Collection) v)) || + (v instanceof Map && Collections.isEmpty((Map) v)) || + (v.getClass().isArray() && Array.getLength(v) == 0); } - @Deprecated //remove just before 1.0.0 - protected void setDate(String name, Date d) { - if (d == null) { - map.remove(name); - } else { - long seconds = d.getTime() / 1000; - map.put(name, seconds); - } + protected Object idiomaticGet(String key) { + return this.idiomaticValues.get(key); } - protected Object setDateAsSeconds(String name, Date d) { - if (d == null) { - return map.remove(name); - } else { - long seconds = d.getTime() / 1000; - return map.put(name, seconds); - } + protected T idiomaticGet(Field field) { + Object value = this.idiomaticValues.get(field.getId()); + return field.cast(value); + } + + @Override + public T get(Field field) { + Assert.notNull(field, "Field cannot be null."); + final String id = Assert.hasText(field.getId(), "Field id cannot be null or empty."); + Object value = idiomaticValues.get(id); + return field.cast(value); } @Override public int size() { - return map.size(); + return values.size(); } @Override public boolean isEmpty() { - return map.isEmpty(); + return values.isEmpty(); } @Override public boolean containsKey(Object o) { - return map.containsKey(o); + return values.containsKey(o); } @Override public boolean containsValue(Object o) { - return map.containsValue(o); + return values.containsValue(o); } @Override public Object get(Object o) { - return map.get(o); + return values.get(o); + } + + /** + * Convenience method to put a value for a canonical field. + * + * @param field the field representing the property name to set + * @param value the value to set + * @return the previous value for the field name, or {@code null} if there was no previous value + * @since JJWT_RELEASE_VERSION + */ + protected Object put(Field field, Object value) { + return put(field.getId(), value); } @Override - public Object put(String s, Object o) { - if (o == null) { - return map.remove(s); - } else { - return map.put(s, o); + public Object put(String name, Object value) { + name = Assert.notNull(Strings.clean(name), "Member name cannot be null or empty."); + if (value instanceof String) { + value = Strings.clean((String) value); + } + return idiomaticPut(name, value); + } + + // ensures that if a property name matches an RFC-specified name, that value can be represented + // as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value. + private Object idiomaticPut(String name, Object value) { + Assert.stateNotNull(name, "Name cannot be null."); // asserted by caller + Field field = FIELDS.get(name); + if (field != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: + return apply(field, value); + } else { //non-standard/custom property: + return nullSafePut(name, value); } } - @Override - public Object remove(Object o) { - return map.remove(o); + protected Object nullSafePut(String name, Object value) { + if (isReducibleToNull(value)) { + return remove(name); + } else { + this.idiomaticValues.put(name, value); + return this.values.put(name, value); + } + } + + protected Object apply(Field field, Object rawValue) { + + final String id = field.getId(); + + if (isReducibleToNull(rawValue)) { + return remove(id); + } + + T idiomaticValue; // preferred Java format + Object canonicalValue; // as required by the RFC + try { + idiomaticValue = field.applyFrom(rawValue); + Assert.notNull(idiomaticValue, "Field's resulting idiomaticValue cannot be null."); + canonicalValue = field.applyTo(idiomaticValue); + Assert.notNull(canonicalValue, "Field's resulting canonicalValue cannot be null."); + } catch (Exception e) { + StringBuilder sb = new StringBuilder(100); + sb.append("Invalid ").append(getName()).append(" ").append(field).append(" value"); + if (field.isSecret()) { + sb.append(": ").append(RedactedSupplier.REDACTED_VALUE); + } else if (!(rawValue instanceof byte[])) { + // don't print raw byte array gibberish. We can't base64[url] encode it either because that could + // make the exception message confusing: the developer would see an encoded string and could think + // that was the rawValue specified when it wasn't. + sb.append(": ").append(Objects.nullSafeToString(rawValue)); + } + sb.append(". ").append(e.getMessage()); + String msg = sb.toString(); + throw new IllegalArgumentException(msg, e); + } + Object retval = nullSafePut(id, canonicalValue); + this.idiomaticValues.put(id, idiomaticValue); + return retval; + } + + @Override + public Object remove(Object key) { + this.idiomaticValues.remove(key); + return this.values.remove(key); } - @SuppressWarnings("NullableProblems") @Override public void putAll(Map m) { if (m == null) { return; } - for (Map.Entry entry : m.entrySet()) { + for (Map.Entry entry : m.entrySet()) { String s = entry.getKey(); put(s, entry.getValue()); } @@ -177,36 +204,38 @@ public class JwtMap implements Map { @Override public void clear() { - map.clear(); + this.values.clear(); + this.idiomaticValues.clear(); } @Override public Set keySet() { - return map.keySet(); + return values.keySet(); } @Override public Collection values() { - return map.values(); + return values.values(); } @Override public Set> entrySet() { - return map.entrySet(); + return values.entrySet(); } @Override public String toString() { - return map.toString(); + return values.toString(); } @Override public int hashCode() { - return map.hashCode(); + return values.hashCode(); } + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override public boolean equals(Object obj) { - return map.equals(obj); + return values.equals(obj); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java new file mode 100644 index 00000000..084bc6f4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java @@ -0,0 +1,93 @@ +/* + * 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.lang.Assert; + +public class JwtTokenizer { + + static final char DELIMITER = '.'; + + private static final String DELIM_ERR_MSG_PREFIX = "Invalid compact JWT string: Compact JWSs must contain " + + "exactly 2 period characters, and compact JWEs must contain exactly 4. Found: "; + + @SuppressWarnings("unchecked") + public T tokenize(String jwt) { + + Assert.hasText(jwt, "Argument cannot be null or empty."); + + String protectedHeader = ""; //Both JWS and JWE + String body = ""; //JWS payload or JWE Ciphertext + String encryptedKey = ""; //JWE only + String iv = ""; //JWE only + String digest; //JWS Signature or JWE AAD Tag + + int delimiterCount = 0; + + StringBuilder sb = new StringBuilder(128); + + for (int i = 0; i < jwt.length(); i++) { + + char c = jwt.charAt(i); + + if (Character.isWhitespace(c)) { + String msg = "Compact JWT strings may not contain whitespace."; + throw new MalformedJwtException(msg); + } + + if (c == DELIMITER) { + + String token = sb.toString(); + + switch (delimiterCount) { + case 0: + protectedHeader = token; + break; + case 1: + body = token; //for JWS + encryptedKey = token; //for JWE + break; + case 2: + body = ""; //clear out value set for JWS + iv = token; + break; + case 3: + body = token; + break; + } + + sb.setLength(0); + delimiterCount++; + } else { + sb.append(c); + } + } + + if (delimiterCount != 2 && delimiterCount != 4) { + String msg = DELIM_ERR_MSG_PREFIX + delimiterCount; + throw new MalformedJwtException(msg); + } + + digest = sb.toString(); + + if (delimiterCount == 2) { + return (T) new DefaultTokenizedJwt(protectedHeader, body, digest); + } + + return (T) new DefaultTokenizedJwe(protectedHeader, body, digest, encryptedKey, iv); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/ProtectedHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/ProtectedHeaderBuilder.java new file mode 100644 index 00000000..1a74d566 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/ProtectedHeaderBuilder.java @@ -0,0 +1,26 @@ +/* + * 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.ProtectedHeader; +import io.jsonwebtoken.ProtectedHeaderMutator; +import io.jsonwebtoken.security.X509Builder; + +// TODO: move this concept to the API when Java 8 is supported. Do we even need it? +public interface ProtectedHeaderBuilder, T extends ProtectedHeaderBuilder> + extends HeaderBuilder, ProtectedHeaderMutator, X509Builder { + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java similarity index 74% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java rename to impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java index a7175070..b9c97826 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwe.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * 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. @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken.impl; -public interface JwtSignatureValidator { +public interface TokenizedJwe extends TokenizedJwt { - boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignature); + String getEncryptedKey(); + + String getIv(); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java new file mode 100644 index 00000000..16d5e86a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/TokenizedJwt.java @@ -0,0 +1,52 @@ +/* + * 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.Header; + +import java.util.Map; + +public interface TokenizedJwt { + + /** + * Protected header. + * + * @return protected header. + */ + String getProtected(); + + /** + * Returns the Payload for a JWS or Ciphertext for a JWE. + * + * @return the Payload for a JWS or Ciphertext for a JWE. + */ + String getBody(); + + /** + * Returns the Signature for JWS or AAD Tag for JWE. + * + * @return the Signature for JWS or AAD Tag for JWE. + */ + String getDigest(); + + /** + * Returns a new {@link Header} instance with the specified map state. + * + * @param m the header state + * @return a new header instance. + */ + Header createHeader(Map m); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/UnprotectedHeaderBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/UnprotectedHeaderBuilder.java new file mode 100644 index 00000000..20b9c46e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/UnprotectedHeaderBuilder.java @@ -0,0 +1,29 @@ +/* + * 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.UnprotectedHeader; + +// TODO: move this concept to the API when Java 8 is supported so we can have UnprotectedHeader.builder() --> returns UnprotectedHeaderBuilder + +/** + * A builder to create {@link UnprotectedHeader} instances. + * + * @since JJWT_RELEASE_VERSION + */ +public interface UnprotectedHeaderBuilder extends HeaderBuilder { + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java index b8b2b577..efda1ccb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java @@ -19,6 +19,7 @@ import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionException; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -32,6 +33,22 @@ import java.io.OutputStream; */ public abstract class AbstractCompressionCodec implements CompressionCodec { + private final String id; + + protected AbstractCompressionCodec(String id) { + this.id = Assert.hasText(Strings.clean(id), "id argument cannot be null or empty."); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getAlgorithmName() { + return getId(); + } + //package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc). //TODO: make protected on a minor release interface StreamWrapper { @@ -58,11 +75,11 @@ public abstract class AbstractCompressionCodec implements CompressionCodec { //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[] payload, StreamWrapper wrapper) throws IOException { + byte[] writeAndClose(byte[] content, StreamWrapper wrapper) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512); OutputStream compressionStream = wrapper.wrap(outputStream); try { - compressionStream.write(payload); + compressionStream.write(content); compressionStream.flush(); } finally { Objects.nullSafeClose(compressionStream); @@ -71,29 +88,29 @@ public abstract class AbstractCompressionCodec implements CompressionCodec { } /** - * Implement this method to do the actual work of compressing the payload + * Implement this method to do the actual work of compressing the content * - * @param payload the bytes to compress + * @param content the bytes to compress * @return the compressed bytes * @throws IOException if the compression causes an IOException */ - protected abstract byte[] doCompress(byte[] payload) throws IOException; + protected abstract byte[] doCompress(byte[] content) throws IOException; /** - * Asserts that payload is not null and calls {@link #doCompress(byte[]) doCompress} + * Asserts that content is not null and calls {@link #doCompress(byte[]) doCompress} * - * @param payload bytes to compress + * @param content bytes to compress * @return compressed bytes * @throws CompressionException if {@link #doCompress(byte[]) doCompress} throws an IOException */ @Override - public final byte[] compress(byte[] payload) { - Assert.notNull(payload, "payload cannot be null."); + public final byte[] compress(byte[] content) { + Assert.notNull(content, "content cannot be null."); try { - return doCompress(payload); + return doCompress(content); } catch (IOException e) { - throw new CompressionException("Unable to compress payload.", e); + throw new CompressionException("Unable to compress content.", e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 6564e248..dd04090b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -20,13 +20,17 @@ import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.CompressionCodecs; import io.jsonwebtoken.CompressionException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.impl.lang.IdRegistry; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Registry; import io.jsonwebtoken.lang.Strings; +import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.LinkedHashSet; +import java.util.Set; /** * Default implementation of {@link CompressionCodecResolver} that supports the following: @@ -36,63 +40,61 @@ import java.util.Map; * nothing and returns {@code null} to the caller, indicating no compression was used. *
  • If the header has a {@code zip} value of {@code DEF}, a {@link DeflateCompressionCodec} will be returned.
  • *
  • If the header has a {@code zip} value of {@code GZIP}, a {@link GzipCompressionCodec} will be returned.
  • - *
  • If the header has any other {@code zip} value, a {@link CompressionException} is thrown to reflect an - * unrecognized algorithm.
  • + *
  • If the header has any other {@code zip} value, it tries to find corresponding {@code CompressionCodec} that + * matches that {@link CompressionCodec#getId() id}. If it finds a match, it returns it.
  • + *
  • If a matching {@code CompressionCodec} is not found for the specified {@code zip} value, + * a {@link CompressionException} is thrown to reflect an unrecognized algorithm.
  • * * *

    If you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement your own * {@link CompressionCodecResolver} and specify that when * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} and - * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    + * {@link io.jsonwebtoken.JwtParserBuilder#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    * * @see DeflateCompressionCodec * @see GzipCompressionCodec * @since 0.6.0 */ -public class DefaultCompressionCodecResolver implements CompressionCodecResolver { +public class DefaultCompressionCodecResolver implements CompressionCodecResolver, Locator { - private static final String MISSING_COMPRESSION_MESSAGE = "Unable to find an implementation for compression algorithm [%s] using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations."; + private static final String MISSING_COMPRESSION_MESSAGE = "Unable to find an implementation for compression " + + "algorithm [%s] using java.util.ServiceLoader or via any specified extra CompressionCodec instances. " + + "Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or " + + "your own .jar for custom implementations, or use the JwtParser.addCompressionCodecs configuration " + + "method."; - private final Map codecs; + private final Registry codecs; public DefaultCompressionCodecResolver() { - Map codecMap = new HashMap<>(); - for (CompressionCodec codec : Services.loadAll(CompressionCodec.class)) { - codecMap.put(codec.getAlgorithmName().toUpperCase(), codec); - } + this(Collections.emptySet()); + } - codecMap.put(CompressionCodecs.DEFLATE.getAlgorithmName().toUpperCase(), CompressionCodecs.DEFLATE); - codecMap.put(CompressionCodecs.GZIP.getAlgorithmName().toUpperCase(), CompressionCodecs.GZIP); - - codecs = Collections.unmodifiableMap(codecMap); + public DefaultCompressionCodecResolver(Collection extraCodecs) { + Assert.notNull(extraCodecs, "extraCodecs cannot be null."); + Set codecs = new LinkedHashSet<>(Services.loadAll(CompressionCodec.class)); + codecs.addAll(extraCodecs); + codecs.add(CompressionCodecs.DEFLATE); // standard ones are added last so they can't be accidentally replaced + codecs.add(CompressionCodecs.GZIP); + this.codecs = new IdRegistry<>("CompressionCodec", codecs); } @Override - public CompressionCodec resolveCompressionCodec(Header header) { - String cmpAlg = getAlgorithmFromHeader(header); - - final boolean hasCompressionAlgorithm = Strings.hasText(cmpAlg); - - if (!hasCompressionAlgorithm) { + public CompressionCodec locate(Header header) { + Assert.notNull(header, "Header cannot be null."); + String id = header.getCompressionAlgorithm(); + if (!Strings.hasText(id)) { return null; } - return byName(cmpAlg); - } - - private String getAlgorithmFromHeader(Header header) { - Assert.notNull(header, "header cannot be null."); - - return header.getCompressionAlgorithm(); - } - - private CompressionCodec byName(String name) { - Assert.hasText(name, "'name' must not be empty"); - - CompressionCodec codec = codecs.get(name.toUpperCase()); + CompressionCodec codec = codecs.find(id); if (codec == null) { - throw new CompressionException(String.format(MISSING_COMPRESSION_MESSAGE, name)); + String msg = String.format(MISSING_COMPRESSION_MESSAGE, id); + throw new CompressionException(msg); } - return codec; } + + @Override + public CompressionCodec resolveCompressionCodec(Header header) { + return locate(header); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index 978ced22..2f1eb543 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -42,14 +42,13 @@ public class DeflateCompressionCodec extends AbstractCompressionCodec { } }; - @Override - public String getAlgorithmName() { - return DEFLATE; + public DeflateCompressionCodec() { + super(DEFLATE); } @Override - protected byte[] doCompress(byte[] payload) throws IOException { - return writeAndClose(payload, WRAPPER); + protected byte[] doCompress(byte[] content) throws IOException { + return writeAndClose(content, WRAPPER); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index 978f1dc0..2b19905d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -39,14 +39,13 @@ public class GzipCompressionCodec extends AbstractCompressionCodec implements Co } }; - @Override - public String getAlgorithmName() { - return GZIP; + public GzipCompressionCodec() { + super(GZIP); } @Override - protected byte[] doCompress(byte[] payload) throws IOException { - return writeAndClose(payload, WRAPPER); + protected byte[] doCompress(byte[] content) throws IOException { + return writeAndClose(content, WRAPPER); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java deleted file mode 100644 index 2a4f746a..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoder; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.lang.Assert; - -import java.nio.charset.Charset; -import java.security.Key; - -public class DefaultJwtSignatureValidator implements JwtSignatureValidator { - - private static final Charset US_ASCII = Charset.forName("US-ASCII"); - - private final SignatureValidator signatureValidator; - private final Decoder base64UrlDecoder; - - @Deprecated - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, key, Decoders.BASE64URL); - } - - public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key, Decoder base64UrlDecoder) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, key, base64UrlDecoder); - } - - @Deprecated - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { - this(factory, alg, key, Decoders.BASE64URL); - } - - public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key, Decoder base64UrlDecoder) { - Assert.notNull(factory, "SignerFactory argument cannot be null."); - Assert.notNull(base64UrlDecoder, "Base64Url decoder argument cannot be null."); - this.signatureValidator = factory.createSignatureValidator(alg, key); - this.base64UrlDecoder = base64UrlDecoder; - } - - @Override - public boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignature) { - - byte[] data = jwtWithoutSignature.getBytes(US_ASCII); - - byte[] signature = base64UrlDecoder.decode(base64UrlEncodedSignature); - - return this.signatureValidator.isValid(data, signature); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java deleted file mode 100644 index 6b8ae49c..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Encoder; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.lang.Assert; - -import java.nio.charset.Charset; -import java.security.Key; - -public class DefaultJwtSigner implements JwtSigner { - - private static final Charset US_ASCII = Charset.forName("US-ASCII"); - - private final Signer signer; - private final Encoder base64UrlEncoder; - - @Deprecated - public DefaultJwtSigner(SignatureAlgorithm alg, Key key) { - this(DefaultSignerFactory.INSTANCE, alg, key, Encoders.BASE64URL); - } - - public DefaultJwtSigner(SignatureAlgorithm alg, Key key, Encoder base64UrlEncoder) { - this(DefaultSignerFactory.INSTANCE, alg, key, base64UrlEncoder); - } - - @Deprecated - public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key) { - this(factory, alg, key, Encoders.BASE64URL); - } - - public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key, Encoder base64UrlEncoder) { - Assert.notNull(factory, "SignerFactory argument cannot be null."); - Assert.notNull(base64UrlEncoder, "Base64Url Encoder cannot be null."); - this.base64UrlEncoder = base64UrlEncoder; - this.signer = factory.createSigner(alg, key); - } - - @Override - public String sign(String jwtWithoutSignature) { - - byte[] bytesToSign = jwtWithoutSignature.getBytes(US_ASCII); - - byte[] signature = signer.sign(bytesToSign); - - return base64UrlEncoder.encode(signature); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java deleted file mode 100644 index 82916847..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - -import java.security.Key; - -public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory { - - public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory(); - - @Override - public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { - Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Signing Key cannot be null."); - - switch (alg) { - case HS256: - case HS384: - case HS512: - return new MacValidator(alg, key); - case RS256: - case RS384: - case RS512: - case PS256: - case PS384: - case PS512: - return new RsaSignatureValidator(alg, key); - case ES256: - case ES384: - case ES512: - return new EllipticCurveSignatureValidator(alg, key); - default: - throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); - } - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java deleted file mode 100644 index 5eee74ce..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - -import java.security.Key; - -public class DefaultSignerFactory implements SignerFactory { - - public static final SignerFactory INSTANCE = new DefaultSignerFactory(); - - @Override - public Signer createSigner(SignatureAlgorithm alg, Key key) { - Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Signing Key cannot be null."); - - switch (alg) { - case HS256: - case HS384: - case HS512: - return new MacSigner(alg, key); - case RS256: - case RS384: - case RS512: - case PS256: - case PS384: - case PS512: - return new RsaSigner(alg, key); - case ES256: - case ES384: - case ES512: - return new EllipticCurveSigner(alg, key); - default: - throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing."); - } - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java deleted file mode 100644 index 15b7f991..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto; - -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; -import io.jsonwebtoken.security.InvalidKeyException; -import io.jsonwebtoken.security.SignatureException; - -import java.math.BigInteger; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.SecureRandom; -import java.security.interfaces.ECKey; -import java.security.spec.ECGenParameterSpec; -import java.util.HashMap; -import java.util.Map; - -/** - * ElliptiCurve crypto provider. - * - * @since 0.5 - */ -public abstract class EllipticCurveProvider extends SignatureProvider { - - private static final Map EC_CURVE_NAMES = createEcCurveNames(); - - private static Map createEcCurveNames() { - Map m = new HashMap<>(); //alg to ASN1 OID name - m.put(SignatureAlgorithm.ES256, "secp256r1"); - m.put(SignatureAlgorithm.ES384, "secp384r1"); - m.put(SignatureAlgorithm.ES512, "secp521r1"); - return m; - } - - protected static String byteSizeString(int bytesLength) { - return bytesLength + " bytes (" + (bytesLength * Byte.SIZE) + " bits)"; - } - - protected final int requiredSignatureByteLength; - protected final int fieldByteLength; - - protected EllipticCurveProvider(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm must be an Elliptic Curve algorithm."); - if (!(key instanceof ECKey)) { - String msg = "Elliptic Curve signatures require an ECKey. The provided key of type " + - key.getClass().getName() + " is not a " + ECKey.class.getName() + " instance."; - throw new InvalidKeyException(msg); - } - this.requiredSignatureByteLength = getSignatureByteArrayLength(alg); - this.fieldByteLength = this.requiredSignatureByteLength / 2; - - ECKey ecKey = (ECKey) key; // can cast here because of the Assert.isTrue assertion above - BigInteger order = ecKey.getParams().getOrder(); - int keyFieldByteLength = (order.bitLength() + 7) / Byte.SIZE; //for ES512 (can be 65 or 66, this ensures 66) - int concatByteLength = keyFieldByteLength * 2; - - if (concatByteLength != this.requiredSignatureByteLength) { - String msg = "EllipticCurve key has a field size of " + - byteSizeString(keyFieldByteLength) + ", but " + alg.name() + " requires a field size of " + - byteSizeString(this.fieldByteLength) + " per [RFC 7518, Section 3.4 (validation)]" + - "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; - throw new InvalidKeyException(msg); - } - } - - /** - * Generates a new secure-random key pair assuming strength enough for the {@link - * SignatureAlgorithm#ES512} algorithm. This is a convenience method that immediately delegates to {@link - * #generateKeyPair(SignatureAlgorithm)} using {@link SignatureAlgorithm#ES512} as the method argument. - * - * @return a new secure-randomly-generated key pair assuming strength enough for the {@link - * SignatureAlgorithm#ES512} algorithm. - * @see #generateKeyPair(SignatureAlgorithm) - * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) - * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair() { - return generateKeyPair(SignatureAlgorithm.ES512); - } - - /** - * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKeyPair(SignatureAlgorithm, SecureRandom)}. - * - * @param alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or {@code ES512} - * @return a new secure-randomly generated key pair of sufficient strength for the specified {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. - * @see #generateKeyPair() - * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) - * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair(SignatureAlgorithm alg) { - return generateKeyPair(alg, DEFAULT_SECURE_RANDOM); - } - - /** - * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator. This is a convenience method that immediately delegates to {@link - * #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom)} using {@code "EC"} as the {@code - * jcaAlgorithmName}. - * - * @param alg alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or {@code - * ES512} - * @param random the SecureRandom generator to use during key generation. - * @return a new secure-randomly generated key pair of sufficient strength for the specified {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator. - * @see #generateKeyPair() - * @see #generateKeyPair(SignatureAlgorithm) - * @see #generateKeyPair(String, String, SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair(SignatureAlgorithm alg, SecureRandom random) { - return generateKeyPair("EC", null, alg, random); - } - - /** - * Generates a new secure-random key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator via the specified JCA provider and algorithm name. - * - * @param jcaAlgorithmName the JCA name of the algorithm to use for key pair generation, for example, {@code - * ECDSA}. - * @param jcaProviderName the JCA provider name of the algorithm implementation (for example {@code "BC"} for - * BouncyCastle) or {@code null} if the default provider should be used. - * @param alg alg the algorithm indicating strength, must be one of {@code ES256}, {@code ES384} or - * {@code ES512} - * @param random the SecureRandom generator to use during key generation. - * @return a new secure-randomly generated key pair of sufficient strength for the specified Elliptic Curve {@link - * SignatureAlgorithm} (must be one of {@code ES256}, {@code ES384} or {@code ES512}) using the specified {@link - * SecureRandom} random number generator via the specified JCA provider and algorithm name. - * @see #generateKeyPair() - * @see #generateKeyPair(SignatureAlgorithm) - * @see #generateKeyPair(SignatureAlgorithm, SecureRandom) - */ - public static KeyPair generateKeyPair(String jcaAlgorithmName, String jcaProviderName, SignatureAlgorithm alg, - SecureRandom random) { - Assert.notNull(alg, "SignatureAlgorithm argument cannot be null."); - Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm argument must represent an Elliptic Curve algorithm."); - try { - KeyPairGenerator g; - - if (Strings.hasText(jcaProviderName)) { - g = KeyPairGenerator.getInstance(jcaAlgorithmName, jcaProviderName); - } else { - g = KeyPairGenerator.getInstance(jcaAlgorithmName); - } - - String paramSpecCurveName = EC_CURVE_NAMES.get(alg); - ECGenParameterSpec spec = new ECGenParameterSpec(paramSpecCurveName); - g.initialize(spec, random); - return g.generateKeyPair(); - } catch (Exception e) { - throw new IllegalStateException("Unable to generate Elliptic Curve KeyPair: " + e.getMessage(), e); - } - } - - /** - * Returns the expected signature byte array length (R + S parts) for - * the specified ECDSA algorithm. Expected lengths are mandated by - * JWA RFC 7518, Section 3.4. - * - * @param alg The ECDSA algorithm. Must be supported and not - * {@code null}. - * @return The expected byte array length for the signature. - * @throws JwtException If the algorithm is not supported. - */ - public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) throws JwtException { - switch (alg) { - case ES256: - return 64; - case ES384: - return 96; - case ES512: - return 132; - default: - throw new JwtException("Unsupported Algorithm: " + alg.name()); - } - } - - - /** - * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated - * R + S format expected by ECDSA JWS. - * - * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. - * @param outputLength The expected length of the ECDSA JWS signature. - * @return The ECDSA JWS encoded signature. - * @throws JwtException If the ASN.1/DER signature format is invalid. - */ - public static byte[] transcodeDERToConcat(final byte[] derSignature, int outputLength) throws JwtException { - - if (derSignature.length < 8 || derSignature[0] != 48) { - throw new JwtException("Invalid ECDSA signature format"); - } - - int offset; - if (derSignature[1] > 0) { - offset = 2; - } else if (derSignature[1] == (byte) 0x81) { - offset = 3; - } else { - throw new JwtException("Invalid ECDSA signature format"); - } - - byte rLength = derSignature[offset + 1]; - - int i = rLength; - while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { - i--; - } - - byte sLength = derSignature[offset + 2 + rLength + 1]; - - int j = sLength; - while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { - j--; - } - - int rawLen = Math.max(i, j); - rawLen = Math.max(rawLen, outputLength / 2); - - if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset - || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength - || derSignature[offset] != 2 - || derSignature[offset + 2 + rLength] != 2) { - throw new JwtException("Invalid ECDSA signature format"); - } - - final byte[] concatSignature = new byte[2 * rawLen]; - - System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); - System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); - - return concatSignature; - } - - - /** - * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by - * the JCA verifier. - * - * @param jwsSignature The JWS signature, consisting of the - * concatenated R and S values. Must not be - * {@code null}. - * @return The ASN.1/DER encoded signature. - * @throws JwtException If the ECDSA JWS signature format is invalid. - */ - public static byte[] transcodeConcatToDER(byte[] jwsSignature) throws JwtException { - try { - return concatToDER(jwsSignature); - } catch (Exception e) { // CVE-2022-21449 guard - String msg = "Invalid ECDSA signature format."; - throw new SignatureException(msg, e); - } - } - - private static byte[] concatToDER(byte[] jwsSignature) throws ArrayIndexOutOfBoundsException { - - int rawLen = jwsSignature.length / 2; - - int i = rawLen; - - while ((i > 0) && (jwsSignature[rawLen - i] == 0)) { - i--; - } - - int j = i; - - if (jwsSignature[rawLen - i] < 0) { - j += 1; - } - - int k = rawLen; - - while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { - k--; - } - - int l = k; - - if (jwsSignature[2 * rawLen - k] < 0) { - l += 1; - } - - int len = 2 + j + 2 + l; - - if (len > 255) { - throw new JwtException("Invalid ECDSA signature format"); - } - - int offset; - - final byte[] derSignature; - - if (len < 128) { - derSignature = new byte[2 + 2 + j + 2 + l]; - offset = 1; - } else { - derSignature = new byte[3 + 2 + j + 2 + l]; - derSignature[1] = (byte) 0x81; - offset = 2; - } - - derSignature[0] = 48; - derSignature[offset++] = (byte) len; - derSignature[offset++] = 2; - derSignature[offset++] = (byte) j; - - System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); - - offset += j; - - derSignature[offset++] = 2; - derSignature[offset++] = (byte) l; - - System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); - - return derSignature; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java deleted file mode 100644 index d9d3c38c..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SignatureException; - -import java.math.BigInteger; -import java.security.Key; -import java.security.PublicKey; -import java.security.Signature; -import java.security.interfaces.ECKey; -import java.security.interfaces.ECPublicKey; -import java.util.Arrays; - -public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator { - - private static final String EC_PUBLIC_KEY_REQD_MSG = - "Elliptic Curve signature validation requires an ECPublicKey instance."; - - private static final String DER_ENCODING_SYS_PROPERTY_NAME = - "io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator.derEncodingSupported"; - - public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG); - } - - @Override - public boolean isValid(byte[] data, byte[] concatSignature) { - Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) key; - try { - // mandated per https://datatracker.ietf.org/doc/html/rfc7518#section-3.4 : - int requiredConcatByteLength = getSignatureByteArrayLength(alg); - - byte[] derSignature; - - if (requiredConcatByteLength != concatSignature.length) { - - /* - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature IFF the application - * is configured to do so. This fallback is for backwards compatibility ONLY (to support tokens - * generated by early versions of jjwt) and backwards compatibility will be removed in a future - * version of this library. This fallback is only enabled if the system property is set to 'true' due to - * the risk of CVE-2022-21449 attacks on early JVM versions 15, 17 and 18. - */ - // TODO: remove for 1.0 (DER-encoding support is not in the JWT RFCs) - if (concatSignature[0] == 0x30 && "true".equalsIgnoreCase(System.getProperty(DER_ENCODING_SYS_PROPERTY_NAME))) { - derSignature = concatSignature; - } else { - String msg = "Provided signature is " + byteSizeString(concatSignature.length) + " but " + - alg.name() + " signatures must be exactly " + byteSizeString(requiredConcatByteLength) + " per " + - "[RFC 7518, Section 3.4 (validation)](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; - throw new SignatureException(msg); - } - } else { - - //guard for JVM security bug CVE-2022-21449: - ECKey ecKey = (ECKey) publicKey; // we can cast here because of the assertions made in the constructor - BigInteger order = ecKey.getParams().getOrder(); - BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, this.fieldByteLength)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, this.fieldByteLength, concatSignature.length)); - if (r.signum() < 1 || s.signum() < 1 || r.compareTo(order) >= 0 || s.compareTo(order) >= 0) { - return false; - } - - // Convert from concat to DER encoding since - // 1) SHAXXXWithECDSAInP1363Format algorithms are only available on >= JDK 9 and - // 2) the SignatureAlgorithm enum JCA alg names are all SHAXXXwithECDSA (which expects DER formatting) - derSignature = transcodeConcatToDER(concatSignature); - } - return doVerify(sig, publicKey, data, derSignature); - } catch (Exception e) { - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) - throws java.security.InvalidKeyException, java.security.SignatureException { - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java deleted file mode 100644 index 16c826fb..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto; - -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.PrivateKey; -import java.security.Signature; - -public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { - - public EllipticCurveSigner(SignatureAlgorithm alg, Key key) { - super(alg, key); - if (!(key instanceof PrivateKey)) { - String msg = "Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of " + - "type " + key.getClass().getName() + " is not an EC PrivateKey."; - throw new io.jsonwebtoken.security.InvalidKeyException(msg); - } - } - - @Override - public byte[] sign(byte[] data) { - try { - return doSign(data); - } catch (InvalidKeyException e) { - throw new SignatureException("Invalid Elliptic Curve PrivateKey. " + e.getMessage(), e); - } catch (java.security.SignatureException e) { - throw new SignatureException("Unable to calculate signature using Elliptic Curve PrivateKey. " + e.getMessage(), e); - } catch (JwtException e) { - throw new SignatureException("Unable to convert signature to JOSE format. " + e.getMessage(), e); - } - } - - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { - PrivateKey privateKey = (PrivateKey)key; - Signature sig = createSignatureInstance(); - sig.initSign(privateKey); - sig.update(data); - return transcodeDERToConcat(sig.sign(), getSignatureByteArrayLength(alg)); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java deleted file mode 100644 index b0117411..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -public abstract class MacProvider extends SignatureProvider { - - protected MacProvider(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isHmac(), "SignatureAlgorithm must be a HMAC SHA algorithm."); - } - - /** - * Generates a new secure-random 512 bit secret key suitable for creating and verifying HMAC-SHA signatures. This - * is a convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link - * SignatureAlgorithm#HS512} as the method argument. - * - * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC-SHA signatures. - * @see #generateKey(SignatureAlgorithm) - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static SecretKey generateKey() { - return generateKey(SignatureAlgorithm.HS512); - } - - /** - * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures - * according to the specified {@code SignatureAlgorithm} using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKey(SignatureAlgorithm, SecureRandom)}. - * - * @param alg the desired signature algorithm - * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using JJWT's default {@link SignatureProvider#DEFAULT_SECURE_RANDOM - * SecureRandom instance}. - * @see #generateKey() - * @see #generateKey(SignatureAlgorithm, SecureRandom) - * @since 0.5 - */ - public static SecretKey generateKey(SignatureAlgorithm alg) { - return generateKey(alg, DEFAULT_SECURE_RANDOM); - } - - /** - * Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures - * according to the specified {@code SignatureAlgorithm} using the specified SecureRandom number generator. This - * implementation returns secure-random key sizes as follows: - * - * - * - *
    Key Sizes
    Signature Algorithm Generated Key Size
    HS256 256 bits (32 bytes)
    HS384 384 bits (48 bytes)
    HS512 512 bits (64 bytes)
    - * - * @param alg the signature algorithm that will be used with the generated key - * @param random the secure random number generator used during key generation - * @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according - * to the specified {@code SignatureAlgorithm} using the specified SecureRandom number generator. - * @see #generateKey() - * @see #generateKey(SignatureAlgorithm) - * @since 0.5 - * @deprecated since 0.10.0 - use {@link #generateKey(SignatureAlgorithm)} instead. - */ - @Deprecated - public static SecretKey generateKey(SignatureAlgorithm alg, SecureRandom random) { - - Assert.isTrue(alg.isHmac(), "SignatureAlgorithm argument must represent an HMAC algorithm."); - - KeyGenerator gen; - - try { - gen = KeyGenerator.getInstance(alg.getJcaName()); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("The " + alg.getJcaName() + " algorithm is not available. " + - "This should never happen on JDK 7 or later - please report this to the JJWT developers.", e); - } - - return gen.generateKey(); - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java deleted file mode 100644 index 07379f53..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SignatureException; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; - -public class MacSigner extends MacProvider implements Signer { - - public MacSigner(SignatureAlgorithm alg, byte[] key) { - this(alg, new SecretKeySpec(key, alg.getJcaName())); - } - - public MacSigner(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isHmac(), "The MacSigner only supports HMAC signature algorithms."); - if (!(key instanceof SecretKey)) { - String msg = "MAC signatures must be computed and verified using a SecretKey. The specified key of " + - "type " + key.getClass().getName() + " is not a SecretKey."; - throw new IllegalArgumentException(msg); - } - } - - @Override - public byte[] sign(byte[] data) { - Mac mac = getMacInstance(); - return mac.doFinal(data); - } - - protected Mac getMacInstance() throws SignatureException { - try { - return doGetMacInstance(); - } catch (NoSuchAlgorithmException e) { - String msg = "Unable to obtain JCA MAC algorithm '" + alg.getJcaName() + "': " + e.getMessage(); - throw new SignatureException(msg, e); - } catch (InvalidKeyException e) { - String msg = "The specified signing key is not a valid " + alg.name() + " key: " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { - Mac mac = Mac.getInstance(alg.getJcaName()); - mac.init(key); - return mac; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java deleted file mode 100644 index bce2b1c7..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidAlgorithmParameterException; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Signature; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; -import java.util.HashMap; -import java.util.Map; - -public abstract class RsaProvider extends SignatureProvider { - - private static final Map PSS_PARAMETER_SPECS = createPssParameterSpecs(); - - private static Map createPssParameterSpecs() { - - Map m = new HashMap(); - - MGF1ParameterSpec ps = MGF1ParameterSpec.SHA256; - PSSParameterSpec spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 32, 1); - m.put(SignatureAlgorithm.PS256, spec); - - ps = MGF1ParameterSpec.SHA384; - spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 48, 1); - m.put(SignatureAlgorithm.PS384, spec); - - ps = MGF1ParameterSpec.SHA512; - spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 64, 1); - m.put(SignatureAlgorithm.PS512, spec); - - return m; - } - - static { - RuntimeEnvironment.enableBouncyCastleIfPossible(); //PS256, PS384, PS512 on <= JDK 10 require BC - } - - protected RsaProvider(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(alg.isRsa(), "SignatureAlgorithm must be an RSASSA or RSASSA-PSS algorithm."); - } - - protected Signature createSignatureInstance() { - - Signature sig = super.createSignatureInstance(); - - PSSParameterSpec spec = PSS_PARAMETER_SPECS.get(alg); - if (spec != null) { - setParameter(sig, spec); - } - return sig; - } - - protected void setParameter(Signature sig, PSSParameterSpec spec) { - try { - doSetParameter(sig, spec); - } catch (InvalidAlgorithmParameterException e) { - String msg = "Unsupported RSASSA-PSS parameter '" + spec + "': " + e.getMessage(); - throw new SignatureException(msg, e); - } - } - - protected void doSetParameter(Signature sig, PSSParameterSpec spec) throws InvalidAlgorithmParameterException { - sig.setParameter(spec); - } - - /** - * Generates a new RSA secure-random 4096 bit key pair. 4096 bits is JJWT's current recommended minimum key size - * for use in modern applications (during or after year 2015). This is a convenience method that immediately - * delegates to {@link #generateKeyPair(int)}. - * - * @return a new RSA secure-random 4096 bit key pair. - * @see #generateKeyPair(int) - * @see #generateKeyPair(int, SecureRandom) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.5 - */ - public static KeyPair generateKeyPair() { - return generateKeyPair(4096); - } - - /** - * Generates a new RSA secure-randomly key pair of the specified size using JJWT's default {@link - * SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method that immediately - * delegates to {@link #generateKeyPair(int, SecureRandom)}. - * - * @param keySizeInBits the key size in bits (NOT bytes). - * @return a new RSA secure-random key pair of the specified size. - * @see #generateKeyPair() - * @see #generateKeyPair(int, SecureRandom) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.5 - */ - public static KeyPair generateKeyPair(int keySizeInBits) { - return generateKeyPair(keySizeInBits, DEFAULT_SECURE_RANDOM); - } - - /** - * Generates a new RSA secure-randomly key pair suitable for the specified SignatureAlgorithm using JJWT's - * default {@link SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method - * that immediately delegates to {@link #generateKeyPair(int)} based on the relevant key size for the specified - * algorithm. - * - * @param alg the signature algorithm to inspect to determine a size in bits. - * @return a new RSA secure-random key pair of the specified size. - * @see #generateKeyPair() - * @see #generateKeyPair(int, SecureRandom) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.10.0 - */ - @SuppressWarnings("unused") //used by io.jsonwebtoken.security.Keys - public static KeyPair generateKeyPair(SignatureAlgorithm alg) { - Assert.isTrue(alg.isRsa(), "Only RSA algorithms are supported by this method."); - int keySizeInBits = 4096; - switch (alg) { - case RS256: - case PS256: - keySizeInBits = 2048; - break; - case RS384: - case PS384: - keySizeInBits = 3072; - break; - } - return generateKeyPair(keySizeInBits, DEFAULT_SECURE_RANDOM); - } - - /** - * Generates a new RSA secure-random key pair of the specified size using the given SecureRandom number generator. - * This is a convenience method that immediately delegates to {@link #generateKeyPair(String, int, SecureRandom)} - * using {@code RSA} as the {@code jcaAlgorithmName} argument. - * - * @param keySizeInBits the key size in bits (NOT bytes) - * @param random the secure random number generator to use during key generation. - * @return a new RSA secure-random key pair of the specified size using the given SecureRandom number generator. - * @see #generateKeyPair() - * @see #generateKeyPair(int) - * @see #generateKeyPair(String, int, SecureRandom) - * @since 0.5 - */ - public static KeyPair generateKeyPair(int keySizeInBits, SecureRandom random) { - return generateKeyPair("RSA", keySizeInBits, random); - } - - /** - * Generates a new secure-random key pair of the specified size using the specified SecureRandom according to the - * specified {@code jcaAlgorithmName}. - * - * @param jcaAlgorithmName the name of the JCA algorithm to use for key pair generation, for example, {@code RSA}. - * @param keySizeInBits the key size in bits (NOT bytes) - * @param random the SecureRandom generator to use during key generation. - * @return a new secure-randomly generated key pair of the specified size using the specified SecureRandom according - * to the specified {@code jcaAlgorithmName}. - * @see #generateKeyPair() - * @see #generateKeyPair(int) - * @see #generateKeyPair(int, SecureRandom) - * @since 0.5 - */ - protected static KeyPair generateKeyPair(String jcaAlgorithmName, int keySizeInBits, SecureRandom random) { - KeyPairGenerator keyGenerator; - try { - keyGenerator = KeyPairGenerator.getInstance(jcaAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Unable to obtain an RSA KeyPairGenerator: " + e.getMessage(), e); - } - - keyGenerator.initialize(keySizeInBits, random); - - return keyGenerator.genKeyPair(); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java deleted file mode 100644 index ba36be1b..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.MessageDigest; -import java.security.PublicKey; -import java.security.Signature; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; - -public class RsaSignatureValidator extends RsaProvider implements SignatureValidator { - - private final RsaSigner SIGNER; - - public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { - super(alg, key); - Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey, - "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance."); - this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null; - } - - @Override - public boolean isValid(byte[] data, byte[] signature) { - if (key instanceof PublicKey) { - Signature sig = createSignatureInstance(); - PublicKey publicKey = (PublicKey) key; - try { - return doVerify(sig, publicKey, data, signature); - } catch (Exception e) { - String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); - throw new SignatureException(msg, e); - } - } else { - Assert.notNull(this.SIGNER, "RSA Signer instance cannot be null. This is a bug. Please report it."); - byte[] computed = this.SIGNER.sign(data); - return MessageDigest.isEqual(computed, signature); - } - } - - protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) - throws InvalidKeyException, java.security.SignatureException { - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java deleted file mode 100644 index 6bc87931..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.SignatureException; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.PrivateKey; -import java.security.Signature; -import java.security.interfaces.RSAKey; - -public class RsaSigner extends RsaProvider implements Signer { - - public RsaSigner(SignatureAlgorithm alg, Key key) { - super(alg, key); - // https://github.com/jwtk/jjwt/issues/68 - // Instead of checking for an instance of RSAPrivateKey, check for PrivateKey and RSAKey: - if (!(key instanceof PrivateKey && key instanceof RSAKey)) { - String msg = "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSA PrivateKey."; - throw new IllegalArgumentException(msg); - } - } - - @Override - public byte[] sign(byte[] data) { - try { - return doSign(data); - } catch (InvalidKeyException e) { - throw new SignatureException("Invalid RSA PrivateKey. " + e.getMessage(), e); - } catch (java.security.SignatureException e) { - throw new SignatureException("Unable to calculate signature using RSA PrivateKey. " + e.getMessage(), e); - } - } - - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - PrivateKey privateKey = (PrivateKey)key; - Signature sig = createSignatureInstance(); - sig.initSign(privateKey); - sig.update(data); - return sig.sign(); - } - -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java deleted file mode 100644 index 7419a478..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto; - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; -import io.jsonwebtoken.security.SignatureException; - -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Signature; - -abstract class SignatureProvider { - - /** - * JJWT's default SecureRandom number generator. This RNG is initialized using the JVM default as follows: - * - *
    
    -     * static {
    -     *     DEFAULT_SECURE_RANDOM = new SecureRandom();
    -     *     DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]);
    -     * }
    -     * 
    - * - *

    nextBytes is called to force the RNG to initialize itself if not already initialized. The - * byte array is not used and discarded immediately for garbage collection.

    - */ - public static final SecureRandom DEFAULT_SECURE_RANDOM; - - static { - DEFAULT_SECURE_RANDOM = new SecureRandom(); - DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]); - } - - protected final SignatureAlgorithm alg; - protected final Key key; - - protected SignatureProvider(SignatureAlgorithm alg, Key key) { - Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notNull(key, "Key cannot be null."); - this.alg = alg; - this.key = key; - } - - protected Signature createSignatureInstance() { - try { - return getSignatureInstance(); - } catch (NoSuchAlgorithmException e) { - String msg = "Unavailable " + alg.getFamilyName() + " Signature algorithm '" + alg.getJcaName() + "'."; - if (!alg.isJdkStandard() && !isBouncyCastleAvailable()) { - msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath."; - } - throw new SignatureException(msg, e); - } - } - - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - return Signature.getInstance(alg.getJcaName()); - } - - protected boolean isBouncyCastleAvailable() { - return RuntimeEnvironment.BOUNCY_CASTLE_AVAILABLE; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java new file mode 100644 index 00000000..441f027d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/Codec.java @@ -0,0 +1,53 @@ +/* + * Copyright © 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.impl.lang.Converter; +import io.jsonwebtoken.io.Decoder; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.DecodingException; +import io.jsonwebtoken.io.Encoder; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; + +public class Codec implements Converter { + + public static final Codec BASE64 = new Codec(Encoders.BASE64, Decoders.BASE64); + public static final Codec BASE64URL = new Codec(Encoders.BASE64URL, Decoders.BASE64URL); + + private final Encoder encoder; + private final Decoder decoder; + + public Codec(Encoder encoder, Decoder decoder) { + this.encoder = Assert.notNull(encoder, "Encoder cannot be null."); + this.decoder = Assert.notNull(decoder, "Decoder cannot be null."); + } + + @Override + public String applyTo(byte[] a) { + return this.encoder.encode(a); + } + + @Override + public byte[] applyFrom(String b) { + try { + return this.decoder.decode(b); + } catch (DecodingException e) { + String msg = "Cannot decode input String. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java new file mode 100644 index 00000000..6a075edf --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverter.java @@ -0,0 +1,54 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.Assert; + +import java.math.BigInteger; + +public class BigIntegerUBytesConverter implements Converter { + + private static final String NEGATIVE_MSG = + "JWA Base64urlUInt values MUST be >= 0 (non-negative) per the 'Base64urlUInt' definition in " + + "[JWA RFC 7518, Section 2](https://www.rfc-editor.org/rfc/rfc7518.html#section-2)"; + + @Override + public byte[] applyTo(BigInteger bigInt) { + Assert.notNull(bigInt, "BigInteger argument cannot be null."); + if (BigInteger.ZERO.compareTo(bigInt) > 0) { + throw new IllegalArgumentException(NEGATIVE_MSG); + } + + final int bitLen = bigInt.bitLength(); + final byte[] bytes = bigInt.toByteArray(); + // round bitLen. This gives the minimal number of bytes necessary to represent an unsigned byte array: + final int unsignedByteLen = Math.max(1, (bitLen + 7) / Byte.SIZE); + + if (bytes.length == unsignedByteLen) { // already in the form we need + return bytes; + } + //otherwise, we need to strip the sign byte (start copying at index 1 instead of 0): + byte[] ubytes = new byte[unsignedByteLen]; + System.arraycopy(bytes, 1, ubytes, 0, unsignedByteLen); + return ubytes; + } + + @Override + public BigInteger applyFrom(byte[] bytes) { + Assert.notEmpty(bytes, "Byte array cannot be null or empty."); + return new BigInteger(1, bytes); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java new file mode 100644 index 00000000..0052122c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -0,0 +1,191 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; + +public final class Bytes { + + public static final byte[] EMPTY = new byte[0]; + + private static final int LONG_BYTE_LENGTH = Long.SIZE / Byte.SIZE; + private static final int INT_BYTE_LENGTH = Integer.SIZE / Byte.SIZE; + public static final String LONG_REQD_MSG = "Long byte arrays must be " + LONG_BYTE_LENGTH + " bytes in length."; + public static final String INT_REQD_MSG = "Integer byte arrays must be " + INT_BYTE_LENGTH + " bytes in length."; + + //prevent instantiation + private Bytes() { + } + + public static byte[] toBytes(int i) { + return new byte[]{ + (byte) (i >>> 24), + (byte) (i >>> 16), + (byte) (i >>> 8), + (byte) i + }; + } + + public static byte[] toBytes(long l) { + return new byte[]{ + (byte) (l >>> 56), + (byte) (l >>> 48), + (byte) (l >>> 40), + (byte) (l >>> 32), + (byte) (l >>> 24), + (byte) (l >>> 16), + (byte) (l >>> 8), + (byte) l + }; + } + + public static long toLong(byte[] bytes) { + Assert.isTrue(Arrays.length(bytes) == LONG_BYTE_LENGTH, LONG_REQD_MSG); + return ((bytes[0] & 0xFFL) << 56) | + ((bytes[1] & 0xFFL) << 48) | + ((bytes[2] & 0xFFL) << 40) | + ((bytes[3] & 0xFFL) << 32) | + ((bytes[4] & 0xFFL) << 24) | + ((bytes[5] & 0xFFL) << 16) | + ((bytes[6] & 0xFFL) << 8) | + (bytes[7] & 0xFFL); + } + + public static int toInt(byte[] bytes) { + Assert.isTrue(Arrays.length(bytes) == INT_BYTE_LENGTH, INT_REQD_MSG); + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + } + + public static int indexOf(byte[] source, byte[] target) { + return indexOf(source, target, 0); + } + + public static int indexOf(byte[] source, byte[] target, int fromIndex) { + return indexOf(source, 0, length(source), target, 0, length(target), fromIndex); + } + + + static int indexOf(byte[] source, int srcOffset, int srcLen, + byte[] target, int targetOffset, int targetLen, + int fromIndex) { + + if (fromIndex >= srcLen) { + return (targetLen == 0 ? srcLen : -1); + } + if (fromIndex < 0) { + fromIndex = 0; + } + if (targetLen == 0) { + return fromIndex; + } + + byte first = target[targetOffset]; + int max = srcOffset + (srcLen - targetLen); + + for (int i = srcOffset + fromIndex; i <= max; i++) { // + + if (source[i] != first) { // continue on to find the first matching byte + while (++i <= max && source[i] != first) ; + } + + if (i <= max) { // found first byte in target, now try to find the rest: + int j = i + 1; + int end = j + targetLen - 1; + //noinspection StatementWithEmptyBody + for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++) ; + if (j == end) { + return i - srcOffset; // found entire target byte array + } + } + } + return -1; + } + + public static boolean startsWith(byte[] src, byte[] prefix) { + return startsWith(src, prefix, 0); + } + + public static boolean startsWith(byte[] src, byte[] prefix, int offset) { + int to = offset; + int po = 0; + int pc = length(prefix); + if ((offset < 0) || (offset > length(src) - pc)) { + return false; + } + while (--pc >= 0) { + if (src[to++] != prefix[po++]) { + return false; + } + } + return true; + } + + public static boolean endsWith(byte[] src, byte[] suffix) { + return startsWith(src, suffix, length(src) - length(suffix)); + } + + public static byte[] concat(byte[]... arrays) { + int len = 0; + int numArrays = Arrays.length(arrays); + for (int i = 0; i < numArrays; i++) { + len += length(arrays[i]); + } + byte[] output = new byte[len]; + int position = 0; + if (len > 0) { + for (byte[] array : arrays) { + int alen = length(array); + if (alen > 0) { + System.arraycopy(array, 0, output, position, alen); + position += alen; + } + } + } + return output; + } + + public static boolean isEmpty(byte[] bytes) { + return length(bytes) == 0; + } + + public static int length(byte[] bytes) { + return bytes == null ? 0 : bytes.length; + } + + public static long bitLength(byte[] bytes) { + return length(bytes) * (long) Byte.SIZE; + } + + public static String bitsMsg(long bitLength) { + return bitLength + " bits (" + bitLength / Byte.SIZE + " bytes)"; + } + + public static String bytesMsg(int byteArrayLength) { + return bitsMsg((long) byteArrayLength * Byte.SIZE); + } + + public static void increment(byte[] a) { + for (int i = a.length - 1; i >= 0; --i) { + if (++a[i] != 0) { + break; + } + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java new file mode 100644 index 00000000..0cdfd4e3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedFunction.java @@ -0,0 +1,20 @@ +/* + * Copyright © 2020 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 CheckedFunction { + R apply(T t) throws Exception; +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java similarity index 73% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java rename to impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java index 18600f24..c5dd8008 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CheckedSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * Copyright © 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. @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken.impl.lang; -import io.jsonwebtoken.security.SignatureException; +/** + * @since JJWT_RELEASE_VERSION + */ +public interface CheckedSupplier { -public interface Signer { - - byte[] sign(byte[] data) throws SignatureException; + T get() throws Exception; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java new file mode 100644 index 00000000..41f50b32 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CollectionConverter.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +class CollectionConverter> implements Converter { + + private final Converter elementConverter; + private final Function fn; + + public static CollectionConverter> forList(Converter elementConverter) { + return new CollectionConverter<>(elementConverter, new CreateListFunction()); + } + + public static CollectionConverter> forSet(Converter elementConverter) { + return new CollectionConverter<>(elementConverter, new CreateSetFunction()); + } + + public CollectionConverter(Converter elementConverter, Function fn) { + this.elementConverter = Assert.notNull(elementConverter, "Element converter cannot be null."); + this.fn = Assert.notNull(fn, "Collection function cannot be null."); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Object applyTo(C ts) { + if (Collections.isEmpty(ts)) { + return ts; + } + Collection c = fn.apply(ts.size()); + for (T element : ts) { + Object encoded = elementConverter.applyTo(element); + c.add(encoded); + } + return c; + } + + private C toElementList(Collection c) { + Assert.notEmpty(c, "Collection cannot be null or empty."); + C result = fn.apply(c.size()); + for (Object o : c) { + T element = elementConverter.applyFrom(o); + result.add(element); + } + return result; + } + + @Override + public C applyFrom(Object value) { + if (value == null) { + return null; + } + Collection c; + if (value.getClass().isArray() && !value.getClass().getComponentType().isPrimitive()) { + c = Collections.arrayToList(value); + } else if (value instanceof Collection) { + c = (Collection) value; + } else { + c = java.util.Collections.singletonList(value); + } + C result; + if (Collections.isEmpty(c)) { + result = fn.apply(0); + } else { + result = toElementList(c); + } + return result; + } + + private static class CreateListFunction implements Function> { + @Override + public List apply(Integer size) { + return size > 0 ? new ArrayList(size) : new ArrayList(); + } + } + + private static class CreateSetFunction implements Function> { + @Override + public Set apply(Integer size) { + return size > 0 ? new LinkedHashSet(size) : new LinkedHashSet(); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java new file mode 100644 index 00000000..8b129af3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverter.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +public final class CompactMediaTypeIdConverter implements Converter { + + public static final Converter INSTANCE = new CompactMediaTypeIdConverter(); + + private static final String APP_MEDIA_TYPE_PREFIX = "application/"; + + static String compactIfPossible(String cty) { + Assert.hasText(cty, "Value cannot be null or empty."); + if (Strings.startsWithIgnoreCase(cty, APP_MEDIA_TYPE_PREFIX)) { + // per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 + // we can only use the compact form if no other '/' exists in the string + for (int i = cty.length() - 1; i >= APP_MEDIA_TYPE_PREFIX.length(); i--) { + char c = cty.charAt(i); + if (c == '/') { + return cty; // found another '/', can't compact, so just return unmodified + } + } + // no additional '/' found, we can strip the prefix: + return cty.substring(APP_MEDIA_TYPE_PREFIX.length()); + } + return cty; // didn't start with 'application/', so we can't trim it - just return unmodified + } + + @Override + public Object applyTo(String s) { + return compactIfPossible(s); + } + + @Override + public String applyFrom(Object o) { + Assert.notNull(o, "Value cannot be null."); + Assert.isInstanceOf(String.class, o, "Value must be a string."); + String s = (String) o; + return compactIfPossible(s); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java new file mode 100644 index 00000000..1de314f2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/CompoundConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.Assert; + +public class CompoundConverter implements Converter { + + private final Converter first; + private final Converter second; + + public CompoundConverter(Converter first, Converter second) { + this.first = Assert.notNull(first, "First converter cannot be null."); + this.second = Assert.notNull(second, "Second converter cannot be null."); + } + + @Override + public C applyTo(A a) { + B b = first.applyTo(a); + return second.applyTo(b); + } + + @Override + public A applyFrom(C c) { + B b = second.applyFrom(c); + return first.applyFrom(b); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java new file mode 100644 index 00000000..3d7316b4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Condition.java @@ -0,0 +1,23 @@ +/* + * Copyright © 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.lang; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface Condition { + boolean test(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java new file mode 100644 index 00000000..4a90e457 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Conditions.java @@ -0,0 +1,90 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.Assert; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class Conditions { + + private Conditions() { + } + + public static final Condition TRUE = of(true); + + public static Condition of(boolean val) { + return new BooleanCondition(val); + } + + public static Condition not(Condition c) { + return new NotCondition(c); + } + + public static Condition exists(CheckedSupplier s) { + return new ExistsCondition(s); + } + + public static Condition notExists(CheckedSupplier s) { + return not(exists(s)); + } + + private static final class NotCondition implements Condition { + + private final Condition c; + + private NotCondition(Condition c) { + this.c = Assert.notNull(c, "Condition cannot be null."); + } + + @Override + public boolean test() { + return !c.test(); + } + } + + private static final class BooleanCondition implements Condition { + private final boolean value; + + public BooleanCondition(boolean value) { + this.value = value; + } + + @Override + public boolean test() { + return value; + } + } + + private static final class ExistsCondition implements Condition { + private final CheckedSupplier supplier; + + ExistsCondition(CheckedSupplier supplier) { + this.supplier = Assert.notNull(supplier, "CheckedSupplier cannot be null."); + } + + @Override + public boolean test() { + Object value = null; + try { + value = supplier.get(); + } catch (Exception ignored) { + } + return value != null; + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java new file mode 100644 index 00000000..2c56e6dd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ConstantFunction.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2020 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; + + +/** + * Function that always returns the same value + * + * @param Input type + * @param Return value type + */ +public final class ConstantFunction implements Function { + + private static final Function NULL = new ConstantFunction<>(null); + + private final R value; + + public ConstantFunction(R value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + public static Function forNull() { + return (Function) NULL; + } + + @Override + public R apply(T t) { + return this.value; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java similarity index 79% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java rename to impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java index 450b707f..638b601c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * Copyright © 2020 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken.impl.lang; -public interface JwtSigner { +public interface Converter { - String sign(String jwtWithoutSignature); + B applyTo(A a); + + A applyFrom(B b); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java new file mode 100644 index 00000000..138808bd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Converters.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.impl.io.Codec; +import io.jsonwebtoken.impl.security.JwtX509StringConverter; + +import java.math.BigInteger; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +public final class Converters { + + public static final Converter URI = Converters.forEncoded(URI.class, new UriStringConverter()); + + public static final Converter BASE64URL_BYTES = Converters.forEncoded(byte[].class, Codec.BASE64URL); + + public static final Converter X509_CERTIFICATE = + Converters.forEncoded(X509Certificate.class, JwtX509StringConverter.INSTANCE); + + public static final Converter BIGINT_UBYTES = new BigIntegerUBytesConverter(); + public static final Converter BIGINT = Converters.forEncoded(BigInteger.class, + compound(BIGINT_UBYTES, Codec.BASE64URL)); + + //prevent instantiation + private Converters() { + } + + public static Converter forType(Class clazz) { + return new RequiredTypeConverter<>(clazz); + } + + public static Converter, Object> forSet(Converter elementConverter) { + return CollectionConverter.forSet(elementConverter); + } + + public static Converter, Object> forList(Converter elementConverter) { + return CollectionConverter.forList(elementConverter); + } + + public static Converter forEncoded(Class elementType, Converter elementConverter) { + return new EncodedObjectConverter<>(elementType, elementConverter); + } + + public static Converter compound(final Converter aConv, final Converter bConv) { + return new CompoundConverter<>(aConv, bConv); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java new file mode 100644 index 00000000..dd1d0248 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultField.java @@ -0,0 +1,126 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; + +public class DefaultField implements Field { + + private final String ID; + private final String NAME; + private final boolean SECRET; + private final Class IDIOMATIC_TYPE; // data type, or if collection, element type + private final Class> COLLECTION_TYPE; // null if field doesn't represent collection + private final Converter CONVERTER; + + public DefaultField(String id, String name, boolean secret, + Class idiomaticType, + Class> collectionType, + Converter converter) { + this.ID = Strings.clean(Assert.hasText(id, "ID argument cannot be null or empty.")); + this.NAME = Strings.clean(Assert.hasText(name, "Name argument cannot be null or empty.")); + this.IDIOMATIC_TYPE = Assert.notNull(idiomaticType, "idiomaticType argument cannot be null."); + this.CONVERTER = Assert.notNull(converter, "Converter argument cannot be null."); + this.SECRET = secret; + this.COLLECTION_TYPE = collectionType; // can be null if field isn't a collection + } + + @Override + public String getId() { + return this.ID; + } + + @Override + public String getName() { + return this.NAME; + } + + @Override + public boolean supports(Object value) { + if (value == null) { + return true; + } + if (COLLECTION_TYPE != null && COLLECTION_TYPE.isInstance(value)) { + Collection c = COLLECTION_TYPE.cast(value); + return c.isEmpty() || IDIOMATIC_TYPE.isInstance(c.iterator().next()); + } + return IDIOMATIC_TYPE.isInstance(value); + } + + @SuppressWarnings("unchecked") + @Override + public T cast(Object value) { + if (value != null) { + if (COLLECTION_TYPE != null) { // field represents a collection, ensure it and its elements are the expected type: + if (!COLLECTION_TYPE.isInstance(value)) { + String msg = "Cannot cast " + value.getClass().getName() + " to " + + COLLECTION_TYPE.getName() + "<" + IDIOMATIC_TYPE.getName() + ">"; + throw new ClassCastException(msg); + } + Collection c = COLLECTION_TYPE.cast(value); + if (!c.isEmpty()) { + Object element = c.iterator().next(); + if (!IDIOMATIC_TYPE.isInstance(element)) { + String msg = "Cannot cast " + value.getClass().getName() + " to " + + COLLECTION_TYPE.getName() + "<" + IDIOMATIC_TYPE.getName() + ">: At least one " + + "element is not an instance of " + IDIOMATIC_TYPE.getName(); + throw new ClassCastException(msg); + } + } + } else if (!IDIOMATIC_TYPE.isInstance(value)) { + String msg = "Cannot cast " + value.getClass().getName() + " to " + IDIOMATIC_TYPE.getName(); + throw new ClassCastException(msg); + } + } + return (T) value; + } + + @Override + public boolean isSecret() { + return SECRET; + } + + @Override + public int hashCode() { + return this.ID.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Field) { + return this.ID.equals(((Field) obj).getId()); + } + return false; + } + + @Override + public String toString() { + return "'" + this.ID + "' (" + this.NAME + ")"; + } + + @Override + public Object applyTo(T t) { + return CONVERTER.applyTo(t); + } + + @Override + public T applyFrom(Object o) { + return CONVERTER.applyFrom(o); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java new file mode 100644 index 00000000..e7014402 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultFieldBuilder.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 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; + +import io.jsonwebtoken.lang.Assert; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class DefaultFieldBuilder implements FieldBuilder { + + private String id; + private String name; + private boolean secret; + private final Class type; + private Converter converter; + private Class> collectionType; // will be null if field doesn't represent a collection (list or set) + + public DefaultFieldBuilder(Class type) { + this.type = Assert.notNull(type, "Type cannot be null."); + } + + @Override + public FieldBuilder setId(String id) { + this.id = id; + return this; + } + + @Override + public FieldBuilder setName(String name) { + this.name = name; + return this; + } + + @Override + public FieldBuilder setSecret(boolean secret) { + this.secret = secret; + return this; + } + + @SuppressWarnings("unchecked") + @Override + public FieldBuilder> list() { + Class clazz = List.class; + this.collectionType = (Class>) clazz; + return (FieldBuilder>) this; + } + + @SuppressWarnings("unchecked") + @Override + public FieldBuilder> set() { + Class clazz = Set.class; + this.collectionType = (Class>) clazz; + return (FieldBuilder>) this; + } + + @Override + public FieldBuilder setConverter(Converter converter) { + this.converter = converter; + return this; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Field build() { + Assert.notNull(this.type, "Type must be set."); + Converter conv = this.converter; + if (conv == null) { + conv = Converters.forType(this.type); + } + if (this.collectionType != null) { + conv = List.class.isAssignableFrom(collectionType) ? Converters.forList(conv) : Converters.forSet(conv); + } + if (this.secret) { + conv = new RedactedValueConverter(conv); + } + return new DefaultField<>(this.id, this.name, this.secret, this.type, this.collectionType, conv); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java new file mode 100644 index 00000000..54dd81c2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java @@ -0,0 +1,72 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Registry; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +public class DefaultRegistry implements Registry, Function { + + private final Map VALUES; + + private final String qualifiedKeyName; + + public DefaultRegistry(String name, String keyName, Collection values, Function keyFn) { + name = Assert.hasText(Strings.clean(name), "name cannot be null or empty."); + keyName = Assert.hasText(Strings.clean(keyName), "keyName cannot be null or empty."); + this.qualifiedKeyName = name + " " + keyName; + Assert.notEmpty(values, "Collection of values may not be null or empty."); + Assert.notNull(keyFn, "Key function cannot be null."); + Map m = new LinkedHashMap<>(values.size()); + for (V value : values) { + K key = Assert.notNull(keyFn.apply(value), "Key function cannot return a null value."); + m.put(key, value); + } + this.VALUES = Collections.immutable(m); + } + + @Override + public V apply(K k) { + Assert.notNull(k, this.qualifiedKeyName + " cannot be null."); + return VALUES.get(k); + } + + @Override + public Collection values() { + return VALUES.values(); + } + + @Override + public V get(K key) { + V value = find(key); + if (value == null) { + String msg = "Unrecognized " + this.qualifiedKeyName + ": " + key; + throw new IllegalArgumentException(msg); + } + return value; + } + + @Override + public V find(K key) { + return apply(key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java new file mode 100644 index 00000000..0c9c843d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/DelegatingCheckedFunction.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2022 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 class DelegatingCheckedFunction implements CheckedFunction { + + final Function delegate; + + public DelegatingCheckedFunction(Function delegate) { + this.delegate = delegate; + } + + @Override + public R apply(T t) throws Exception { + return delegate.apply(t); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java new file mode 100644 index 00000000..c6f0f33e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/EncodedObjectConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.lang.Assert; + +public class EncodedObjectConverter implements Converter { + + private final Class type; + private final Converter converter; + + public EncodedObjectConverter(Class type, Converter converter) { + this.type = Assert.notNull(type, "Value type cannot be null."); + this.converter = Assert.notNull(converter, "Value converter cannot be null."); + } + + @Override + public Object applyTo(T t) { + Assert.notNull(t, "Value argument cannot be null."); + return converter.applyTo(t); + } + + @Override + public T applyFrom(Object value) { + Assert.notNull(value, "Value argument cannot be null."); + if (type.isInstance(value)) { + return type.cast(value); + } else if (value instanceof String) { + return converter.applyFrom((String) value); + } else { + String msg = "Values must be either String or " + type.getName() + + " instances. Value type found: " + value.getClass().getName() + "."; + throw new IllegalArgumentException(msg); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java new file mode 100644 index 00000000..609d3480 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Field.java @@ -0,0 +1,29 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.Identifiable; + +public interface Field extends Identifiable, Converter { + + String getName(); + + boolean supports(Object value); + + T cast(Object value); + + boolean isSecret(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java new file mode 100644 index 00000000..075acda6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.Builder; + +import java.util.List; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public interface FieldBuilder extends Builder> { + + FieldBuilder setId(String id); + + FieldBuilder setName(String name); + + FieldBuilder setSecret(boolean secret); + + FieldBuilder> list(); + + FieldBuilder> set(); + + FieldBuilder setConverter(Converter converter); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java similarity index 77% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java rename to impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java index edeccb7e..759b2191 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FieldReadable.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * Copyright © 2022 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken.impl.lang; -public interface SignatureValidator { - - boolean isValid(byte[] data, byte[] signature); +public interface FieldReadable { + T get(Field field); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java new file mode 100644 index 00000000..c121fad3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java @@ -0,0 +1,67 @@ +/* + * Copyright © 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.lang; + +import java.math.BigInteger; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; +import java.util.Set; + +public final class Fields { + + private Fields() { // prevent instantiation + } + + public static Field string(String id, String name) { + return builder(String.class).setId(id).setName(name).build(); + } + + public static Field rfcDate(String id, String name) { + return builder(Date.class).setConverter(JwtDateConverter.INSTANCE).setId(id).setName(name).build(); + } + + public static Field> x509Chain(String id, String name) { + return builder(X509Certificate.class) + .setConverter(Converters.X509_CERTIFICATE).list() + .setId(id).setName(name).build(); + } + + public static FieldBuilder builder(Class type) { + return new DefaultFieldBuilder<>(type); + } + + public static Field> stringSet(String id, String name) { + return builder(String.class).set().setId(id).setName(name).build(); + } + + public static Field uri(String id, String name) { + return builder(URI.class).setConverter(Converters.URI).setId(id).setName(name).build(); + } + + public static FieldBuilder bytes(String id, String name) { + return builder(byte[].class).setConverter(Converters.BASE64URL_BYTES).setId(id).setName(name); + } + + public static FieldBuilder bigInt(String id, String name) { + return builder(BigInteger.class).setConverter(Converters.BIGINT).setId(id).setName(name); + } + + public static Field secretBigInt(String id, String name) { + return bigInt(id, name).setSecret(true).build(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java new file mode 100644 index 00000000..58423453 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; + +public class FormattedStringFunction implements Function { + + private final String msg; + + public FormattedStringFunction(String msg) { + this.msg = Assert.hasText(msg, "msg argument cannot be null or empty."); + } + + @Override + public String apply(T arg) { + return String.format(msg, arg); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java new file mode 100644 index 00000000..967a6cef --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/FormattedStringSupplier.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Supplier; + +public class FormattedStringSupplier implements Supplier { + + private final String msg; + + private final Object[] args; + + public FormattedStringSupplier(String msg, Object[] args) { + this.msg = Assert.hasText(msg, "Message cannot be null or empty."); + this.args = Assert.notEmpty(args, "Arguments cannot be null or empty."); + } + + @Override + public String get() { + return String.format(this.msg, this.args); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java new file mode 100644 index 00000000..4afd81f2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Function.java @@ -0,0 +1,21 @@ +/* + * Copyright © 2020 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 Function { + + R apply(T t); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java new file mode 100644 index 00000000..418d74fe --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Functions.java @@ -0,0 +1,110 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; + +public final class Functions { + + private Functions() { + } + + public static Function forNull() { + return ConstantFunction.forNull(); + } + + /** + * Wraps the specified function to ensure that if any exception occurs, it is of the specified type and/or with + * the specified message. If no exception occurs, the function's return value is returned as expected. + * + *

    If {@code fn} throws an exception, its type is checked. If it is already of type {@code exClass}, that + * exception is immediately thrown. If it is not the expected exception type, a message is created with the + * specified {@code msg} template, and a new exception of the specified type is thrown with the formatted message, + * using the original exception as its cause.

    + * + * @param fn the function to execute + * @param exClass the exception type expected, if any + * @param msg the formatted message to use if throwing a new exception, used as the first argument to {@link String#format(String, Object...) String.format}. + * @param the function argument type + * @param the function's return type + * @param type of exception to ensure + * @return the wrapping function instance. + */ + public static Function wrapFmt(CheckedFunction fn, Class exClass, String msg) { + return new PropagatingExceptionFunction<>(fn, exClass, new FormattedStringFunction(msg)); + } + + public static Function wrap(Function fn, Class exClass, String fmt, Object... args) { + return new PropagatingExceptionFunction<>(new DelegatingCheckedFunction<>(fn), exClass, new FormattedStringSupplier(fmt, args)); + } + + /** + * Returns a composed function that first applies the {@code before} function to its input, and then applies + * the {@code after} function to the result. If evaluation of either function throws an exception, it is relayed to + * the caller of the composed function. + * + * @param type of input to the {@code before} function and the resulting composed function. + * @param the type of output of the {@code before} function, and of the input to the {@code after} function. + * @param return type of the {@code after} function and the resulting composed function. + * @param before the function to invoke first + * @param after the function to invoke second with the output from the first + * @return a composed function that first applies the {@code before} function and then + * applies the {@code after} function. + * @throws IllegalArgumentException if either {@code before} or {@code after} are null. + */ + public static Function andThen(final Function before, final Function after) { + Assert.notNull(before, "Before function cannot be null."); + Assert.notNull(after, "After function cannot be null."); + return new Function() { + @Override + public R apply(T t) { + V result = before.apply(t); + return after.apply(result); + } + }; + } + + /** + * Returns a composed function that invokes the specified functions in iteration order, and returns the first + * non-null result. Once a non-null result is discovered, no further functions will be invoked, 'short-circuiting' + * any remaining functions. If evaluation of any function throws an exception, it is relayed to the caller of the + * composed function. + * + * @param the type of input of the functions, and of the composed function + * @param the type of output of the functions, and of the composed function + * @param fns the functions to iterate + * @return a composed function that invokes the specified functions in iteration order, returning the first non-null + * result. + * @throws NullPointerException if after is null + */ + @SafeVarargs + public static Function firstResult(final Function... fns) { + Assert.notEmpty(fns, "Function list cannot be null or empty."); + return new Function() { + @Override + public R apply(T t) { + for (Function fn : fns) { + Assert.notNull(fn, "Function cannot be null."); + R result = fn.apply(t); + if (result != null) { + return result; + } + } + return null; + } + }; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java new file mode 100644 index 00000000..4da8f9fc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; + +public class IdRegistry extends StringRegistry { + + private static final Function FN = new Function() { + @Override + public String apply(Identifiable identifiable) { + Assert.notNull(identifiable, "Identifiable argument cannot be null."); + return Assert.notNull(Strings.clean(identifiable.getId()), "Identifier cannot be null or empty."); + } + }; + + @SuppressWarnings("unchecked") + public IdRegistry(String name, Collection instances) { + super(name, "id", + Assert.notEmpty(instances, "Collection of Identifiable instances may not be null or empty."), + (Function) FN); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java new file mode 100644 index 00000000..2dc15c25 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -0,0 +1,111 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.DateFormats; + +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; + +public class JwtDateConverter implements Converter { + + public static final JwtDateConverter INSTANCE = new JwtDateConverter(); + + @Override + public Object applyTo(Date date) { + if (date == null) { + return null; + } + // https://www.rfc-editor.org/rfc/rfc7519.html#section-2, 'Numeric Date' definition: + return date.getTime() / 1000L; + } + + @Override + public Date applyFrom(Object o) { + return toSpecDate(o); + } + + /** + * Returns an RFC-compatible {@link Date} equivalent of the specified object value using heuristics. + * + * @param value object to convert to a {@code Date} using heuristics. + * @return an RFC-compatible {@link Date} equivalent of the specified object value using heuristics. + * @since 0.10.0 + */ + public static Date toSpecDate(Object value) { + if (value == null) { + return null; + } + if (value instanceof String) { + try { + value = Long.parseLong((String) value); + } catch (NumberFormatException ignored) { // will try in the fallback toDate method call below + } + } + if (value instanceof Number) { + // https://github.com/jwtk/jjwt/issues/122: + // The JWT RFC *mandates* NumericDate values are represented as seconds. + // Because java.util.Date requires milliseconds, we need to multiply by 1000: + long seconds = ((Number) value).longValue(); + value = seconds * 1000; + } + //v would have been normalized to milliseconds if it was a number value, so perform normal date conversion: + return toDate(value); + } + + /** + * Returns a {@link Date} equivalent of the specified object value using heuristics. + * + * @param v the object value to represent as a Date. + * @return a {@link Date} equivalent of the specified object value using heuristics. + */ + public static Date toDate(Object v) { + if (v == null) { + return null; + } else if (v instanceof Date) { + return (Date) v; + } else if (v instanceof Calendar) { //since 0.10.0 + return ((Calendar) v).getTime(); + } else if (v instanceof Number) { + //assume millis: + long millis = ((Number) v).longValue(); + return new Date(millis); + } else if (v instanceof String) { + return parseIso8601Date((String) v); //ISO-8601 parsing since 0.10.0 + } else { + String msg = "Cannot create Date from object of type " + v.getClass().getName() + "."; + throw new IllegalArgumentException(msg); + } + } + + /** + * Parses the specified ISO-8601-formatted string and returns the corresponding {@link Date} instance. + * + * @param value an ISO-8601-formatted string. + * @return a {@link Date} instance reflecting the specified ISO-8601-formatted string. + * @since 0.10.0 + */ + private static Date parseIso8601Date(String value) throws IllegalArgumentException { + try { + return DateFormats.parseIso8601Date(value); + } catch (ParseException e) { + String msg = "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. " + + "All heuristics exhausted. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/LegacyServices.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/LegacyServices.java index 26845bdc..ec0e1048 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/LegacyServices.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/LegacyServices.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2019 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; import io.jsonwebtoken.lang.Classes; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java new file mode 100644 index 00000000..0a76a5aa --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/LocatorFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.lang.Assert; + +public class LocatorFunction implements Function, T> { + + private final Locator locator; + + public LocatorFunction(Locator locator) { + this.locator = Assert.notNull(locator, "Locator instance cannot be null."); + } + + @Override + public T apply(Header h) { + return this.locator.locate(h); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java new file mode 100644 index 00000000..2699f8bb --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Nameable.java @@ -0,0 +1,21 @@ +/* + * Copyright © 2022 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 Nameable { + + String getName(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java new file mode 100644 index 00000000..1547f6b7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/NullSafeConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.lang.Assert; + +public class NullSafeConverter implements Converter { + + private final Converter converter; + + public NullSafeConverter(Converter converter) { + this.converter = Assert.notNull(converter, "Delegate converter cannot be null."); + } + + @Override + public B applyTo(A a) { + return a == null ? null : converter.applyTo(a); + } + + @Override + public A applyFrom(B b) { + return b == null ? null : converter.applyFrom(b); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/OptionalCtorInvoker.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/OptionalCtorInvoker.java new file mode 100644 index 00000000..2225980f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/OptionalCtorInvoker.java @@ -0,0 +1,72 @@ +/* + * 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; + +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +public class OptionalCtorInvoker extends ReflectionFunction { + + private final Constructor CTOR; + + public OptionalCtorInvoker(String fqcn, Object... ctorArgTypesOrFqcns) { + Assert.hasText(fqcn, "fqcn cannot be null."); + Constructor ctor = null; + try { + Class clazz = Classes.forName(fqcn); + Class[] ctorArgTypes = null; + if (Arrays.length(ctorArgTypesOrFqcns) > 0) { + ctorArgTypes = new Class[ctorArgTypesOrFqcns.length]; + List> l = new ArrayList<>(ctorArgTypesOrFqcns.length); + for (Object ctorArgTypeOrFqcn : ctorArgTypesOrFqcns) { + Class ctorArgClass; + if (ctorArgTypeOrFqcn instanceof Class) { + ctorArgClass = (Class) ctorArgTypeOrFqcn; + } else { + String typeFqcn = Assert.isInstanceOf(String.class, ctorArgTypeOrFqcn, "ctorArgTypesOrFcqns array must contain Class or String instances."); + ctorArgClass = Classes.forName(typeFqcn); + } + l.add(ctorArgClass); + } + ctorArgTypes = l.toArray(ctorArgTypes); + } + ctor = Classes.getConstructor(clazz, ctorArgTypes); + } catch (Exception ignored) { + } + this.CTOR = ctor; + } + + @Override + protected boolean supports(Object input) { + return CTOR != null; + } + + @Override + protected T invoke(Object input) { + Object[] args = null; + if (input instanceof Object[]) { + args = (Object[]) input; + } else if (input != null) { + args = new Object[]{input}; + } + return Classes.instantiate(CTOR, args); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/OptionalMethodInvoker.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/OptionalMethodInvoker.java new file mode 100644 index 00000000..f816b582 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/OptionalMethodInvoker.java @@ -0,0 +1,50 @@ +/* + * 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; + +import io.jsonwebtoken.lang.Classes; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class OptionalMethodInvoker extends ReflectionFunction { + + private final Class CLASS; + private final Method METHOD; + + public OptionalMethodInvoker(String fqcn, String methodName) { + Class clazz = null; + Method method = null; + try { + clazz = Classes.forName(fqcn); + method = clazz.getMethod(methodName, (Class[]) null); + } catch (Exception ignored) { + } + this.CLASS = clazz; + this.METHOD = method; + } + + @Override + protected boolean supports(T input) { + return CLASS != null && METHOD != null && CLASS.isInstance(input); + } + + @SuppressWarnings("unchecked") + @Override + protected R invoke(T input) throws InvocationTargetException, IllegalAccessException { + return (R) METHOD.invoke(input); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java new file mode 100644 index 00000000..e3bc007e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PositiveIntegerConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; + +import java.util.concurrent.atomic.AtomicInteger; + +public class PositiveIntegerConverter implements Converter { + + public static final PositiveIntegerConverter INSTANCE = new PositiveIntegerConverter(); + + @Override + public Object applyTo(Integer integer) { + return integer; + } + + @Override + public Integer applyFrom(Object o) { + Assert.notNull(o, "Argument cannot be null."); + int i; + if (o instanceof Byte || o instanceof Short || o instanceof Integer || o instanceof AtomicInteger) { + i = ((Number) o).intValue(); + } else { // could be Long, AtomicLong, Float, Decimal, BigInteger, BigDecimal, String, etc., all of which + // may not be accurately converted into an Integer, either due to overflow or fractional values. The + // easiest way to account for all of them is to parse the string value as an int instead of testing all + // the types: + String sval = String.valueOf(o); + try { + i = Integer.parseInt(sval); + } catch (NumberFormatException e) { + String msg = "Value cannot be represented as a java.lang.Integer."; + throw new IllegalArgumentException(msg, e); + } + } + if (i <= 0) { + String msg = "Value must be a positive integer."; + throw new IllegalArgumentException(msg); + } + return i; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java new file mode 100644 index 00000000..d20735bd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/PropagatingExceptionFunction.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; +import io.jsonwebtoken.lang.Supplier; + +import java.lang.reflect.Constructor; + +public class PropagatingExceptionFunction implements Function { + + private final CheckedFunction function; + + private final Function msgFunction; + private final Class clazz; + + public PropagatingExceptionFunction(Function f, Class exceptionClass, String msg) { + this(new DelegatingCheckedFunction<>(f), exceptionClass, new ConstantFunction(msg)); + } + + public PropagatingExceptionFunction(CheckedFunction fn, Class exceptionClass, final Supplier msgSupplier) { + this(fn, exceptionClass, new Function() { + @Override + public String apply(T t) { + return msgSupplier.get(); + } + }); + } + + public PropagatingExceptionFunction(CheckedFunction f, Class exceptionClass, Function msgFunction) { + this.clazz = Assert.notNull(exceptionClass, "Exception class cannot be null."); + this.msgFunction = Assert.notNull(msgFunction, "msgFunction cannot be null."); + this.function = Assert.notNull(f, "Function cannot be null"); + } + + @SuppressWarnings("unchecked") + public R apply(T t) { + try { + return function.apply(t); + } catch (Exception e) { + if (clazz.isAssignableFrom(e.getClass())) { + throw clazz.cast(e); + } + String msg = this.msgFunction.apply(t); + if (!msg.endsWith(".")) { + msg += "."; + } + msg += " Cause: " + e.getMessage(); + Class clazzz = (Class) clazz; + Constructor ctor = Classes.getConstructor(clazzz, String.class, Throwable.class); + throw Classes.instantiate(ctor, msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java new file mode 100644 index 00000000..8b7a0d24 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedSupplier.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Supplier; + +public class RedactedSupplier implements Supplier { + + public static final String REDACTED_VALUE = ""; + + private final T value; + + public RedactedSupplier(T value) { + this.value = Assert.notNull(value, "value cannot be null."); + } + + @Override + public T get() { + return value; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(value); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof RedactedSupplier) { + obj = ((RedactedSupplier) obj).value; // get the wrapped value + } + return Objects.nullSafeEquals(this.value, obj); + } + + @Override + public String toString() { + return REDACTED_VALUE; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java new file mode 100644 index 00000000..12b46aab --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RedactedValueConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Supplier; + +public class RedactedValueConverter implements Converter { + + private final Converter delegate; + + public RedactedValueConverter(Converter delegate) { + this.delegate = Assert.notNull(delegate, "Delegate cannot be null."); + } + + @Override + public Object applyTo(T t) { + Object value = this.delegate.applyTo(t); + if (value != null && !(value instanceof RedactedSupplier)) { + value = new RedactedSupplier<>(value); + } + return value; + } + + @Override + public T applyFrom(Object o) { + if (o instanceof RedactedSupplier) { + o = ((Supplier) o).get(); + } + return this.delegate.applyFrom(o); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/ReflectionFunction.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/ReflectionFunction.java new file mode 100644 index 00000000..bf2225e1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ReflectionFunction.java @@ -0,0 +1,40 @@ +/* + * 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; + +abstract class ReflectionFunction implements Function { + + public static final String ERR_MSG = "Reflection operation failed. This is likely due to an internal " + + "implementation programming error. Please report this to the JJWT development team. Cause: "; + + protected abstract boolean supports(T input); + + protected abstract R invoke(T input) throws Throwable; + + @Override + public final R apply(T input) { + if (supports(input)) { + try { + return invoke(input); + } catch (Throwable throwable) { + // should never happen if supportsInput is true since that would mean we're using the API incorrectly + String msg = ERR_MSG + throwable.getMessage(); + throw new IllegalStateException(msg, throwable); + } + } + return null; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java new file mode 100644 index 00000000..6fa3daf7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredBitLengthConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; + +public class RequiredBitLengthConverter implements Converter { + + private final Converter converter; + + private final int bitLength; + private final boolean exact; + + public RequiredBitLengthConverter(Converter converter, int bitLength) { + this(converter, bitLength, true); + } + + public RequiredBitLengthConverter(Converter converter, int bitLength, boolean exact) { + this.converter = Assert.notNull(converter, "Converter cannot be null."); + this.bitLength = Assert.gt(bitLength, 0, "bitLength must be greater than 0"); + this.exact = exact; + } + + private byte[] assertLength(byte[] bytes) { + long len = Bytes.bitLength(bytes); + if (exact && len != this.bitLength) { + String msg = "Byte array must be exactly " + Bytes.bitsMsg(this.bitLength) + ". Found " + Bytes.bitsMsg(len); + throw new IllegalArgumentException(msg); + } else if (len < this.bitLength) { + String msg = "Byte array must be at least " + Bytes.bitsMsg(this.bitLength) + ". Found " + Bytes.bitsMsg(len); + throw new IllegalArgumentException(msg); + } + return bytes; + } + + @Override + public Object applyTo(byte[] bytes) { + assertLength(bytes); + return this.converter.applyTo(bytes); + } + + @Override + public byte[] applyFrom(Object o) { + byte[] result = this.converter.applyFrom(o); + return assertLength(result); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java new file mode 100644 index 00000000..7cb3f909 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredFieldReader.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.security.JwkContext; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.MalformedKeyException; + +public class RequiredFieldReader implements FieldReadable { + + private final FieldReadable src; + + public RequiredFieldReader(Header header) { + this(Assert.isInstanceOf(FieldReadable.class, header, "Header implementations must implement FieldReadable.")); + } + + public RequiredFieldReader(FieldReadable src) { + this.src = Assert.notNull(src, "Source FieldReadable cannot be null."); + Assert.isInstanceOf(Nameable.class, src, "FieldReadable implementations must implement Nameable."); + } + + private String name() { + return ((Nameable) this.src).getName(); + } + + private JwtException malformed(String msg) { + if (this.src instanceof JwkContext || this.src instanceof Jwk) { + return new MalformedKeyException(msg); + } else { + return new MalformedJwtException(msg); + } + } + + @Override + public T get(Field field) { + T value = this.src.get(field); + if (value == null) { + String msg = name() + " is missing required " + field + " value."; + throw malformed(msg); + } + return value; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java new file mode 100644 index 00000000..fca83b4d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/RequiredTypeConverter.java @@ -0,0 +1,48 @@ +/* + * Copyright © 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.lang; + +import io.jsonwebtoken.lang.Assert; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class RequiredTypeConverter implements Converter { + + private final Class type; + + public RequiredTypeConverter(Class type) { + this.type = Assert.notNull(type, "type argument cannot be null."); + } + + @Override + public Object applyTo(T t) { + return t; + } + + @Override + public T applyFrom(Object o) { + if (o == null) { + return null; + } + Class clazz = o.getClass(); + if (!type.isAssignableFrom(clazz)) { + String msg = "Unsupported value type. Expected: " + type.getName() + ", found: " + clazz.getName(); + throw new IllegalArgumentException(msg); + } + return type.cast(o); + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/StringRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/StringRegistry.java new file mode 100644 index 00000000..6f7630b8 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/StringRegistry.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2022 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; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +public class StringRegistry extends DefaultRegistry { + + private final Function CI_FN; + + private final Map CI_VALUES; + + public StringRegistry(String name, String keyName, Collection values, Function keyFn) { + this(name, keyName, values, keyFn, Locale.ENGLISH); + } + + public StringRegistry(String name, String keyName, Collection values, Function keyFn, final Locale caseInsensitiveLocale) { + super(name, keyName, values, keyFn); + this.CI_FN = new CaseInsensitiveFunction(caseInsensitiveLocale); + Map m = new LinkedHashMap<>(values().size()); + for (V value : values) { + String key = keyFn.apply(value); + key = this.CI_FN.apply(key); + m.put(key, value); + } + this.CI_VALUES = Collections.immutable(m); + } + + @Override + public V apply(String id) { + Assert.hasText(id, "id argument cannot be null or empty."); + V instance = super.apply(id); //try standard ID lookup first. This will satisfy 99% of invocations + if (instance == null) { // fall back to case-insensitive ID lookup: + id = CI_FN.apply(id); + instance = CI_VALUES.get(id); + } + return instance; + } + + private static final class CaseInsensitiveFunction implements Function { + private final Locale LOCALE; + + private CaseInsensitiveFunction(Locale locale) { + this.LOCALE = Assert.notNull(locale, "Case insensitive Locale argument cannot be null."); + } + + @Override + public String apply(String s) { + s = Assert.notNull(Strings.clean(s), "String identifier cannot be null or empty."); + return s.toUpperCase(LOCALE); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java index 3fe9eaa4..098d3b09 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/UnavailableImplementationException.java @@ -17,13 +17,16 @@ package io.jsonwebtoken.impl.lang; /** * Exception indicating that no implementation of an jjwt-api SPI was found on the classpath. + * * @since 0.11.0 */ public final class UnavailableImplementationException extends RuntimeException { - private static final String DEFAULT_NOT_FOUND_MESSAGE = "Unable to find an implementation for %s using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations."; + private static final String DEFAULT_NOT_FOUND_MESSAGE = "Unable to find an implementation for %s using " + + "java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, " + + "for example jjwt-impl.jar, or your own .jar for custom implementations."; - UnavailableImplementationException(final Class klass) { + UnavailableImplementationException(final Class klass) { super(String.format(DEFAULT_NOT_FOUND_MESSAGE, klass)); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java new file mode 100644 index 00000000..6b20ba9c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/UriStringConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2020 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; + +import io.jsonwebtoken.lang.Assert; + +import java.net.URI; + +public class UriStringConverter implements Converter { + + @Override + public String applyTo(URI uri) { + Assert.notNull(uri, "URI cannot be null."); + return uri.toString(); + } + + @Override + public URI applyFrom(String s) { + Assert.hasText(s, "URI string cannot be null or empty."); + try { + return URI.create(s); + } catch (Exception e) { + String msg = "Unable to convert String value '" + s + "' to URI instance: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java new file mode 100644 index 00000000..f1f634af --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2020 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; + +import java.math.BigInteger; +import java.util.Map; + +public interface ValueGetter { + + String getRequiredString(String key); + + int getRequiredInteger(String key); + + int getRequiredPositiveInteger(String key); + + byte[] getRequiredBytes(String key); + + byte[] getRequiredBytes(String key, int requiredByteLength); + + BigInteger getRequiredBigInt(String key, boolean sensitive); + + Map getRequiredMap(String key); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java new file mode 100644 index 00000000..4e51aef9 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwk.java @@ -0,0 +1,67 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.AsymmetricJwk; + +import java.net.URI; +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +public abstract class AbstractAsymmetricJwk extends AbstractJwk implements AsymmetricJwk { + + static final Field USE = Fields.string("use", "Public Key Use"); + public static final Field> X5C = Fields.x509Chain("x5c", "X.509 Certificate Chain"); + public static final Field X5T = Fields.bytes("x5t", "X.509 Certificate SHA-1 Thumbprint").build(); + public static final Field X5T_S256 = Fields.bytes("x5t#S256", "X.509 Certificate SHA-256 Thumbprint").build(); + public static final Field X5U = Fields.uri("x5u", "X.509 URL"); + static final Set> FIELDS = Collections.concat(AbstractJwk.FIELDS, USE, X5C, X5T, X5T_S256, X5U); + + AbstractAsymmetricJwk(JwkContext ctx, List> thumbprintFields) { + super(ctx, thumbprintFields); + } + + @Override + public String getPublicKeyUse() { + return this.context.getPublicKeyUse(); + } + + @Override + public URI getX509Url() { + return this.context.getX509Url(); + } + + @Override + public List getX509CertificateChain() { + return Collections.immutable(this.context.getX509CertificateChain()); + } + + @Override + public byte[] getX509CertificateSha1Thumbprint() { + return (byte[])Arrays.copy(this.context.getX509CertificateSha1Thumbprint()); + } + + @Override + public byte[] getX509CertificateSha256Thumbprint() { + return (byte[])Arrays.copy(this.context.getX509CertificateSha256Thumbprint()); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java new file mode 100644 index 00000000..90c526dc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilder.java @@ -0,0 +1,252 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AsymmetricJwk; +import io.jsonwebtoken.security.AsymmetricJwkBuilder; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPrivateJwkBuilder; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.EcPublicJwkBuilder; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.OctetPrivateJwk; +import io.jsonwebtoken.security.OctetPrivateJwkBuilder; +import io.jsonwebtoken.security.OctetPublicJwk; +import io.jsonwebtoken.security.OctetPublicJwkBuilder; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PrivateJwkBuilder; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.PublicJwkBuilder; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPrivateJwkBuilder; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.RsaPublicJwkBuilder; + +import java.net.URI; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.List; + +abstract class AbstractAsymmetricJwkBuilder, + T extends AsymmetricJwkBuilder> + extends AbstractJwkBuilder implements AsymmetricJwkBuilder { + + protected Boolean applyX509KeyUse = null; + private KeyUseStrategy keyUseStrategy = DefaultKeyUseStrategy.INSTANCE; + + private final DefaultX509Builder x509Builder; + + public AbstractAsymmetricJwkBuilder(JwkContext ctx) { + super(ctx); + this.x509Builder = new DefaultX509Builder<>(this.jwkContext, self(), MalformedKeyException.class); + } + + AbstractAsymmetricJwkBuilder(AbstractAsymmetricJwkBuilder b, JwkContext ctx) { + this(ctx); + this.applyX509KeyUse = b.applyX509KeyUse; + this.keyUseStrategy = b.keyUseStrategy; + } + + @Override + public T setPublicKeyUse(String use) { + Assert.hasText(use, "publicKeyUse cannot be null or empty."); + this.jwkContext.setPublicKeyUse(use); + return self(); + } + + /* + public T setKeyUseStrategy(KeyUseStrategy strategy) { + this.keyUseStrategy = Assert.notNull(strategy, "KeyUseStrategy cannot be null."); + return tthis(); + } + */ + + @Override + public T setX509CertificateChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be null or empty."); + return this.x509Builder.setX509CertificateChain(chain); + } + + @Override + public T setX509Url(URI uri) { + Assert.notNull(uri, "X509Url cannot be null."); + return this.x509Builder.setX509Url(uri); + } + + /* + @Override + public T withX509KeyUse(boolean enable) { + this.applyX509KeyUse = enable; + return tthis(); + } + */ + + @Override + public T setX509CertificateSha1Thumbprint(byte[] thumbprint) { + return this.x509Builder.setX509CertificateSha1Thumbprint(thumbprint); + } + + @Override + public T setX509CertificateSha256Thumbprint(byte[] thumbprint) { + return this.x509Builder.setX509CertificateSha256Thumbprint(thumbprint); + } + + @Override + public T withX509Sha1Thumbprint(boolean enable) { + return this.x509Builder.withX509Sha1Thumbprint(enable); + } + + @Override + public T withX509Sha256Thumbprint(boolean enable) { + return this.x509Builder.withX509Sha256Thumbprint(enable); + } + + @Override + public J build() { + this.x509Builder.apply(); + return super.build(); + } + + private abstract static class DefaultPublicJwkBuilder, M extends PrivateJwk, P extends PrivateJwkBuilder, + T extends PublicJwkBuilder> + extends AbstractAsymmetricJwkBuilder + implements PublicJwkBuilder { + + DefaultPublicJwkBuilder(JwkContext ctx) { + super(ctx); + } + + @Override + public P setPrivateKey(L privateKey) { + Assert.notNull(privateKey, "PrivateKey argument cannot be null."); + final K publicKey = Assert.notNull(jwkContext.getKey(), "PublicKey cannot be null."); + return newPrivateBuilder(newContext(privateKey)).setPublicKey(publicKey); + } + + protected abstract P newPrivateBuilder(JwkContext ctx); + } + + private abstract static class DefaultPrivateJwkBuilder, M extends PrivateJwk, + T extends PrivateJwkBuilder> + extends AbstractAsymmetricJwkBuilder + implements PrivateJwkBuilder { + + DefaultPrivateJwkBuilder(JwkContext ctx) { + super(ctx); + } + + DefaultPrivateJwkBuilder(DefaultPublicJwkBuilder b, JwkContext ctx) { + super(b, ctx); + this.jwkContext.setPublicKey(b.jwkContext.getKey()); + } + + @Override + public T setPublicKey(L publicKey) { + this.jwkContext.setPublicKey(publicKey); + return self(); + } + } + + static class DefaultRsaPublicJwkBuilder + extends DefaultPublicJwkBuilder + implements RsaPublicJwkBuilder { + + DefaultRsaPublicJwkBuilder(JwkContext ctx) { + super(ctx); + } + + @Override + protected RsaPrivateJwkBuilder newPrivateBuilder(JwkContext ctx) { + return new DefaultRsaPrivateJwkBuilder(this, ctx); + } + } + + static class DefaultEcPublicJwkBuilder + extends DefaultPublicJwkBuilder + implements EcPublicJwkBuilder { + DefaultEcPublicJwkBuilder(JwkContext src) { + super(src); + } + + @Override + protected EcPrivateJwkBuilder newPrivateBuilder(JwkContext ctx) { + return new DefaultEcPrivateJwkBuilder(this, ctx); + } + } + + static class DefaultOctetPublicJwkBuilder
    + extends DefaultPublicJwkBuilder, OctetPrivateJwk, + OctetPrivateJwkBuilder, OctetPublicJwkBuilder> + implements OctetPublicJwkBuilder { + DefaultOctetPublicJwkBuilder(JwkContext ctx) { + super(ctx); + EdwardsCurve.assertEdwards(ctx.getKey()); + } + + @Override + protected OctetPrivateJwkBuilder newPrivateBuilder(JwkContext ctx) { + return new DefaultOctetPrivateJwkBuilder<>(this, ctx); + } + } + + static class DefaultRsaPrivateJwkBuilder + extends DefaultPrivateJwkBuilder + implements RsaPrivateJwkBuilder { + DefaultRsaPrivateJwkBuilder(JwkContext src) { + super(src); + } + + DefaultRsaPrivateJwkBuilder(DefaultRsaPublicJwkBuilder b, JwkContext ctx) { + super(b, ctx); + } + } + + static class DefaultEcPrivateJwkBuilder + extends DefaultPrivateJwkBuilder + implements EcPrivateJwkBuilder { + DefaultEcPrivateJwkBuilder(JwkContext src) { + super(src); + } + + DefaultEcPrivateJwkBuilder(DefaultEcPublicJwkBuilder b, JwkContext ctx) { + super(b, ctx); + } + } + + static class DefaultOctetPrivateJwkBuilder + extends DefaultPrivateJwkBuilder, OctetPrivateJwk, + OctetPrivateJwkBuilder> implements OctetPrivateJwkBuilder { + DefaultOctetPrivateJwkBuilder(JwkContext src) { + super(src); + EdwardsCurve.assertEdwards(src.getKey()); + } + + DefaultOctetPrivateJwkBuilder(DefaultOctetPublicJwkBuilder b, JwkContext ctx) { + super(b, ctx); + EdwardsCurve.assertEdwards(ctx.getKey()); + EdwardsCurve.assertEdwards(ctx.getPublicKey()); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java new file mode 100644 index 00000000..8e852667 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -0,0 +1,229 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Converters; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; +import java.util.Set; + +abstract class AbstractEcJwkFactory> extends AbstractFamilyJwkFactory { + + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger THREE = BigInteger.valueOf(3); + private static final String UNSUPPORTED_CURVE_MSG = "The specified ECKey curve does not match a JWA standard curve id."; + + protected static ECParameterSpec getCurveByJwaId(String jwaCurveId) { + ECParameterSpec spec = null; + Curve curve = Curves.findById(jwaCurveId); + if (curve instanceof ECCurve) { + ECCurve ecCurve = (ECCurve) curve; + spec = ecCurve.toParameterSpec(); + } + if (spec == null) { + String msg = "Unrecognized JWA curve id '" + jwaCurveId + "'"; + throw new UnsupportedKeyException(msg); + } + return spec; + } + + protected static String getJwaIdByCurve(EllipticCurve curve) { + ECCurve c = Curves.findBy(curve); + if (c == null) { + throw new UnsupportedKeyException(UNSUPPORTED_CURVE_MSG); + } + return c.getId(); + } + + /** + * https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in + * http://www.secg.org/sec1-v2.pdf Section 2.3.5. + * + * @param fieldSize EC field size + * @param coordinate EC point coordinate (e.g. x or y) + * @return A base64Url-encoded String representing the EC field element per the RFC format + */ + // Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5 + static String toOctetString(int fieldSize, BigInteger coordinate) { + byte[] bytes = Converters.BIGINT_UBYTES.applyTo(coordinate); + int mlen = (int) Math.ceil(fieldSize / 8d); + if (mlen > bytes.length) { + byte[] m = new byte[mlen]; + System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length); + bytes = m; + } + return Encoders.BASE64URL.encode(bytes); + } + + /** + * Returns {@code true} if a given elliptic {@code curve} contains the specified {@code point}, {@code false} + * otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) + * Weierstrass form: + *

    + * y2 = x3 + ax + b + *

    + * + * @param curve the Elliptic Curve to check + * @param point a point that may or may not be defined on the specified elliptic curve + * @return {@code true} if a given elliptic curve contains the specified {@code point}, {@code false} otherwise. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + static boolean contains(EllipticCurve curve, ECPoint point) { + + if (ECPoint.POINT_INFINITY.equals(point)) { + return false; + } + + final BigInteger a = curve.getA(); + final BigInteger b = curve.getB(); + final BigInteger x = point.getAffineX(); + final BigInteger y = point.getAffineY(); + + // The reduced Weierstrass form y^2 = x^3 + ax + b reflects an elliptic curve E over any field K (e.g. all real + // numbers or all complex numbers, etc). For computational simplicity, cryptographic (e.g. NIST) elliptic curves + // restrict K to be a field of integers modulo a prime number 'p'. As such, we apply modulo p (the field prime) + // to the equation to account for the restricted field. For a nice overview of the math behind EC curves and + // their application in cryptography, see + // https://web.northeastern.edu/dummit/docs/cryptography_5_elliptic_curves_in_cryptography.pdf + + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + + // Verify the point coordinates are in field range: + if (x.compareTo(BigInteger.ZERO) < 0 || x.compareTo(p) >= 0 || + y.compareTo(BigInteger.ZERO) < 0 || y.compareTo(p) >= 0) { + return false; + } + + // Finally, assert Weierstrass form equality: + final BigInteger lhs = y.modPow(TWO, p); //mod p to account for field prime + final BigInteger rhs = x.modPow(THREE, p).add(a.multiply(x)).add(b).mod(p); //mod p to account for field prime + return lhs.equals(rhs); + } + + /** + * Multiply a point {@code p} by scalar {@code s} on the curve identified by {@code spec}. + * + * @param p the Elliptic Curve point to multiply + * @param s the scalar value to multiply + * @param spec the domain parameters that identify the Elliptic Curve containing point {@code p}. + */ + private static ECPoint multiply(ECPoint p, BigInteger s, ECParameterSpec spec) { + if (ECPoint.POINT_INFINITY.equals(p)) { + return p; + } + + EllipticCurve curve = spec.getCurve(); + BigInteger n = spec.getOrder(); + BigInteger k = s.mod(n); + + ECPoint r0 = ECPoint.POINT_INFINITY; + ECPoint r1 = p; + + // Montgomery Ladder implementation to mitigate side-channel attacks (i.e. an 'add' operation and a 'double' + // operation is calculated for every loop iteration, regardless if the 'add'' is needed or not) + // See: https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder + while (k.compareTo(BigInteger.ZERO) > 0) { + ECPoint temp = add(r0, r1, curve); + r0 = k.testBit(0) ? temp : r0; + r1 = doublePoint(r1, curve); + k = k.shiftRight(1); + } + + return r0; + } + + private static ECPoint add(ECPoint P, ECPoint Q, EllipticCurve curve) { + + if (ECPoint.POINT_INFINITY.equals(P)) { + return Q; + } else if (ECPoint.POINT_INFINITY.equals(Q)) { + return P; + } else if (P.equals(Q)) { + return doublePoint(P, curve); + } + + final BigInteger Px = P.getAffineX(); + final BigInteger Py = P.getAffineY(); + final BigInteger Qx = Q.getAffineX(); + final BigInteger Qy = Q.getAffineY(); + final BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + final BigInteger slope = Qy.subtract(Py).multiply(Qx.subtract(Px).modInverse(prime)).mod(prime); + final BigInteger Rx = slope.pow(2).subtract(Px).subtract(Qx).mod(prime); + final BigInteger Ry = slope.multiply(Px.subtract(Rx)).subtract(Py).mod(prime); + + return new ECPoint(Rx, Ry); + } + + private static ECPoint doublePoint(ECPoint P, EllipticCurve curve) { + + if (ECPoint.POINT_INFINITY.equals(P)) { + return P; + } + + final BigInteger Px = P.getAffineX(); + final BigInteger Py = P.getAffineY(); + final BigInteger p = ((ECFieldFp) curve.getField()).getP(); + final BigInteger a = curve.getA(); + final BigInteger s = THREE.multiply(Px.pow(2)).add(a).mod(p).multiply(TWO.multiply(Py).modInverse(p)).mod(p); + final BigInteger x = s.pow(2).subtract(TWO.multiply(Px)).mod(p); + final BigInteger y = s.multiply(Px.subtract(x)).subtract(Py).mod(p); + + return new ECPoint(x, y); + } + + AbstractEcJwkFactory(Class keyType, Set> fields) { + super(DefaultEcPublicJwk.TYPE_VALUE, keyType, fields); + } + + // visible for testing + protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { + return (ECPublicKey) keyFactory.generatePublic(spec); + } + + protected ECPublicKey derivePublic(final JwkContext ctx) { + final ECPrivateKey key = ctx.getKey(); + final ECParameterSpec params = key.getParams(); + final ECPoint w = multiply(params.getGenerator(), key.getS(), params); + final ECPublicKeySpec spec = new ECPublicKeySpec(w, params); + return generateKey(ctx, ECPublicKey.class, new CheckedFunction() { + @Override + public ECPublicKey apply(KeyFactory kf) { + try { + return derivePublic(kf, spec); + } catch (Exception e) { + String msg = "Unable to derive ECPublicKey from ECPrivateKey: " + e.getMessage(); + throw new UnsupportedKeyException(msg, e); + } + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java new file mode 100644 index 00000000..5842786f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -0,0 +1,114 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.KeyException; + +import java.security.Key; +import java.security.KeyFactory; +import java.util.Set; + +abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { + + protected static void put(JwkContext ctx, Field field, T value) { + ctx.put(field.getId(), field.applyTo(value)); + } + + private final String ktyValue; + private final Class keyType; + private final Set> fields; + + AbstractFamilyJwkFactory(String ktyValue, Class keyType, Set> fields) { + this.ktyValue = Assert.hasText(ktyValue, "keyType argument cannot be null or empty."); + this.keyType = Assert.notNull(keyType, "keyType class cannot be null."); + this.fields = Assert.notEmpty(fields, "Fields collection cannot be null or empty."); + } + + @Override + public String getId() { + return this.ktyValue; + } + + @Override + public boolean supports(Key key) { + return this.keyType.isInstance(key); + } + + @Override + public JwkContext newContext(JwkContext src, K key) { + Assert.notNull(src, "Source JwkContext cannot be null."); + return key != null ? + new DefaultJwkContext<>(this.fields, src, key) : + new DefaultJwkContext(this.fields, src, false); + } + + @Override + public boolean supports(JwkContext ctx) { + return supports(ctx.getKey()) || supportsKeyValues(ctx); + } + + protected boolean supportsKeyValues(JwkContext ctx) { + return this.ktyValue.equals(ctx.getType()); + } + + protected K generateKey(final JwkContext ctx, final CheckedFunction fn) { + return generateKey(ctx, this.keyType, fn); + } + + protected T generateKey(final JwkContext ctx, final Class type, final CheckedFunction fn) { + JcaTemplate template = new JcaTemplate(getId(), ctx.getProvider(), ctx.getRandom()); + return template.withKeyFactory(new CheckedFunction() { + @Override + public T apply(KeyFactory instance) { + try { + return fn.apply(instance); + } catch (KeyException keyException) { + throw keyException; // propagate + } catch (Exception e) { + String msg = "Unable to create " + type.getSimpleName() + " from JWK " + ctx + ": " + e.getMessage(); + throw new InvalidKeyException(msg, e); + } + } + }); + } + + @Override + public final J createJwk(JwkContext ctx) { + Assert.notNull(ctx, "JwkContext argument cannot be null."); + if (!supports(ctx)) { //should be asserted by caller, but assert just in case: + String msg = "Unsupported JwkContext."; + throw new IllegalArgumentException(msg); + } + K key = ctx.getKey(); + if (key != null) { + ctx.setType(this.ktyValue); + return createJwkFromKey(ctx); + } else { + return createJwkFromValues(ctx); + } + } + + //when called, ctx.getKey() is guaranteed to be non-null + protected abstract J createJwkFromKey(JwkContext ctx); + + //when called ctx.getType() is guaranteed to equal this.ktyValue + protected abstract J createJwkFromValues(JwkContext ctx); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java new file mode 100644 index 00000000..61b0aaa2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -0,0 +1,239 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.Nameable; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.lang.Supplier; +import io.jsonwebtoken.security.HashAlgorithm; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkThumbprint; +import io.jsonwebtoken.security.Jwks; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public abstract class AbstractJwk implements Jwk, FieldReadable, Nameable { + + static final Field ALG = Fields.string("alg", "Algorithm"); + public static final Field KID = Fields.string("kid", "Key ID"); + static final Field> KEY_OPS = Fields.stringSet("key_ops", "Key Operations"); + static final Field KTY = Fields.string("kty", "Key Type"); + static final Set> FIELDS = Collections.setOf(ALG, KID, KEY_OPS, KTY); + + public static final String IMMUTABLE_MSG = "JWKs are immutable and may not be modified."; + protected final JwkContext context; + private final List> THUMBPRINT_FIELDS; + + /** + * @param ctx the backing JwkContext containing the JWK field values. + * @param thumbprintFields the required fields to include in the JWK Thumbprint canonical JSON representation, + * sorted in lexicographic order as defined by + *
    RFC 7638, Section 3.2. + */ + AbstractJwk(JwkContext ctx, List> thumbprintFields) { + this.context = Assert.notNull(ctx, "JwkContext cannot be null."); + Assert.isTrue(!ctx.isEmpty(), "JwkContext cannot be empty."); + Assert.hasText(ctx.getType(), "JwkContext type cannot be null or empty."); + Assert.notNull(ctx.getKey(), "JwkContext key cannot be null."); + this.THUMBPRINT_FIELDS = Assert.notEmpty(thumbprintFields, "JWK Thumbprint fields cannot be null or empty."); + HashAlgorithm idThumbprintAlg = ctx.getIdThumbprintAlgorithm(); + if (!Strings.hasText(getId()) && idThumbprintAlg != null) { + JwkThumbprint thumbprint = thumbprint(idThumbprintAlg); + String kid = thumbprint.toString(); + ctx.setId(kid); + } + } + + private String getRequiredThumbprintValue(Field field) { + Object value = get(field.getId()); + if (value instanceof Supplier) { + value = ((Supplier) value).get(); + } + return Assert.isInstanceOf(String.class, value, "Field canonical value is not a String."); + } + + /** + * Returns the JWK's canonically ordered JSON for JWK thumbprint computation as defined by + * RFC 7638, Section 3.2. + * + * @return the JWK's canonically ordered JSON for JWK thumbprint computation. + */ + private String toThumbprintJson() { + StringBuilder sb = new StringBuilder().append('{'); + Iterator> i = this.THUMBPRINT_FIELDS.iterator(); + while (i.hasNext()) { + Field field = i.next(); + String value = getRequiredThumbprintValue(field); + sb.append('"').append(field.getId()).append("\":\"").append(value).append('"'); + if (i.hasNext()) { + sb.append(","); + } + } + sb.append('}'); + return sb.toString(); + } + + @Override + public JwkThumbprint thumbprint() { + return thumbprint(Jwks.HASH.SHA256); + } + + @Override + public JwkThumbprint thumbprint(final HashAlgorithm alg) { + String json = toThumbprintJson(); + Assert.hasText(json, "Canonical JWK Thumbprint JSON cannot be null or empty."); + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + byte[] digest = alg.digest(new DefaultRequest<>(bytes, this.context.getProvider(), this.context.getRandom())); + return new DefaultJwkThumbprint(digest, alg); + } + + @Override + public String getType() { + return this.context.getType(); + } + + @Override + public String getName() { + return this.context.getName(); + } + + @Override + public Set getOperations() { + return Collections.immutable(this.context.getOperations()); + } + + @Override + public String getAlgorithm() { + return this.context.getAlgorithm(); + } + + @Override + public String getId() { + return this.context.getId(); + } + + @Override + public K toKey() { + return this.context.getKey(); + } + + @Override + public int size() { + return this.context.size(); + } + + @Override + public boolean isEmpty() { + return this.context.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.context.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.context.containsValue(value); + } + + @Override + public Object get(Object key) { + Object val = this.context.get(key); + if (val instanceof Map) { + return Collections.immutable((Map) val); + } else if (val instanceof Collection) { + return Collections.immutable((Collection) val); + } else if (Objects.isArray(val)) { + return Arrays.copy(val); + } else { + return val; + } + } + + @Override + public T get(Field field) { + return this.context.get(field); + } + + @Override + public Set keySet() { + return Collections.immutable(this.context.keySet()); + } + + @Override + public Collection values() { + return Collections.immutable(this.context.values()); + } + + @Override + public Set> entrySet() { + return Collections.immutable(this.context.entrySet()); + } + + private static Object immutable() { + throw new UnsupportedOperationException(IMMUTABLE_MSG); + } + + @Override + public Object put(String s, Object o) { + return immutable(); + } + + @Override + public Object remove(Object o) { + return immutable(); + } + + @Override + public void putAll(Map m) { + immutable(); + } + + @Override + public void clear() { + immutable(); + } + + @Override + public String toString() { + return this.context.toString(); + } + + @Override + public int hashCode() { + return this.context.hashCode(); + } + + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override + public boolean equals(Object obj) { + return this.context.equals(obj); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java new file mode 100644 index 00000000..84fa987b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -0,0 +1,162 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.HashAlgorithm; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.SecretJwk; +import io.jsonwebtoken.security.SecretJwkBuilder; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Set; + +abstract class AbstractJwkBuilder, T extends JwkBuilder> implements JwkBuilder { + + protected JwkContext jwkContext; + protected final JwkFactory jwkFactory; + + @SuppressWarnings("unchecked") + protected AbstractJwkBuilder(JwkContext jwkContext) { + this(jwkContext, (JwkFactory) DispatchingJwkFactory.DEFAULT_INSTANCE); + } + + // visible for testing + protected AbstractJwkBuilder(JwkContext context, JwkFactory factory) { + this.jwkFactory = Assert.notNull(factory, "JwkFactory cannot be null."); + setContext(context); + } + + @SuppressWarnings("unchecked") + protected JwkContext newContext(A key) { + return (JwkContext) this.jwkFactory.newContext(this.jwkContext, (K) key); + } + + protected void setContext(JwkContext ctx) { + this.jwkContext = Assert.notNull(ctx, "JwkContext cannot be null."); + } + + @Override + public T setProvider(Provider provider) { + jwkContext.setProvider(provider); + return self(); + } + + @Override + public T setRandom(SecureRandom random) { + jwkContext.setRandom(random); + return self(); + } + + @Override + public T put(String name, Object value) { + jwkContext.put(name, value); + return self(); + } + + @Override + public T putAll(Map values) { + jwkContext.putAll(values); + return self(); + } + + @Override + public T remove(String key) { + jwkContext.remove(key); + return self(); + } + + @Override + public T clear() { + jwkContext.clear(); + return self(); + } + + @Override + public T setAlgorithm(String alg) { + Assert.hasText(alg, "Algorithm cannot be null or empty."); + jwkContext.setAlgorithm(alg); + return self(); + } + + @Override + public T setId(String id) { + Assert.hasText(id, "Id cannot be null or empty."); + jwkContext.setIdThumbprintAlgorithm(null); //clear out any previously set value + jwkContext.setId(id); + return self(); + } + + @Override + public T setIdFromThumbprint() { + return setIdFromThumbprint(Jwks.HASH.SHA256); + } + + @Override + public T setIdFromThumbprint(HashAlgorithm alg) { + Assert.notNull(alg, "Thumbprint HashAlgorithm cannot be null."); + Assert.notNull(alg.getId(), "Thumbprint HashAlgorithm ID cannot be null."); + this.jwkContext.setId(null); // clear out any previous value + this.jwkContext.setIdThumbprintAlgorithm(alg); + return self(); + } + + @Override + public T setOperations(Set ops) { + Assert.notEmpty(ops, "Operations cannot be null or empty."); + jwkContext.setOperations(ops); + return self(); + } + + @SuppressWarnings("unchecked") + protected final T self() { + return (T) this; + } + + @Override + public J build() { + + //should always exist as there isn't a way to set it outside the constructor: + Assert.stateNotNull(this.jwkContext, "JwkContext should always be non-null"); + + K key = this.jwkContext.getKey(); + if (key == null && this.jwkContext.isEmpty()) { + String msg = "A " + Key.class.getName() + " or one or more name/value pairs must be provided to create a JWK."; + throw new IllegalStateException(msg); + } + try { + return jwkFactory.createJwk(this.jwkContext); + } catch (IllegalArgumentException iae) { + //if we get an IAE, it means the builder state wasn't configured enough in order to create + String msg = "Unable to create JWK: " + iae.getMessage(); + throw new MalformedKeyException(msg, iae); + } + } + + static class DefaultSecretJwkBuilder extends AbstractJwkBuilder + implements SecretJwkBuilder { + public DefaultSecretJwkBuilder(JwkContext ctx) { + super(ctx); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java new file mode 100644 index 00000000..e0ea3cf3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java @@ -0,0 +1,50 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPair; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PublicJwk; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.List; + +abstract class AbstractPrivateJwk> extends AbstractAsymmetricJwk implements PrivateJwk { + + private final M publicJwk; + private final KeyPair keyPair; + + AbstractPrivateJwk(JwkContext ctx, List> thumbprintFields, M pubJwk) { + super(ctx, thumbprintFields); + this.publicJwk = Assert.notNull(pubJwk, "PublicJwk instance cannot be null."); + L publicKey = Assert.notNull(pubJwk.toKey(), "PublicJwk key instance cannot be null."); + this.context.setPublicKey(publicKey); + this.keyPair = new DefaultKeyPair<>(publicKey, toKey()); + } + + @Override + public M toPublicJwk() { + return this.publicJwk; + } + + @Override + public KeyPair toKeyPair() { + return this.keyPair; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java new file mode 100644 index 00000000..7319fe05 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java @@ -0,0 +1,28 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.security.PublicJwk; + +import java.security.PublicKey; +import java.util.List; + +abstract class AbstractPublicJwk extends AbstractAsymmetricJwk implements PublicJwk { + AbstractPublicJwk(JwkContext ctx, List> thumbprintFields) { + super(ctx, thumbprintFields); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecureDigestAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecureDigestAlgorithm.java new file mode 100644 index 00000000..7fd369e5 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecureDigestAlgorithm.java @@ -0,0 +1,88 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.VerifySecureDigestRequest; + +import java.security.Key; +import java.security.MessageDigest; + +abstract class AbstractSecureDigestAlgorithm extends CryptoAlgorithm implements SecureDigestAlgorithm { + + AbstractSecureDigestAlgorithm(String id, String jcaName) { + super(id, jcaName); + } + + protected static String keyType(boolean signing) { + return signing ? "signing" : "verification"; + } + + protected abstract void validateKey(Key key, boolean signing); + + @Override + public final byte[] digest(SecureRequest request) throws SecurityException { + Assert.notNull(request, "Request cannot be null."); + final S key = Assert.notNull(request.getKey(), "Request key cannot be null."); + Assert.notEmpty(request.getPayload(), "Request content cannot be null or empty."); + try { + validateKey(key, true); + return doDigest(request); + } catch (SignatureException | KeyException e) { + throw e; //propagate + } catch (Exception e) { + String msg = "Unable to compute " + getId() + " signature with JCA algorithm '" + getJcaName() + "' " + + "using key {" + KeysBridge.toString(key) + "}: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + + protected abstract byte[] doDigest(SecureRequest request) throws Exception; + + @Override + public final boolean verify(VerifySecureDigestRequest request) throws SecurityException { + Assert.notNull(request, "Request cannot be null."); + final V key = Assert.notNull(request.getKey(), "Request key cannot be null."); + Assert.notEmpty(request.getPayload(), "Request content cannot be null or empty."); + Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty."); + try { + validateKey(key, false); + return doVerify(request); + } catch (SignatureException | KeyException e) { + throw e; //propagate + } catch (Exception e) { + String msg = "Unable to verify " + getId() + " signature with JCA algorithm '" + getJcaName() + "' " + + "using key {" + KeysBridge.toString(key) + "}: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + + protected boolean messageDigest(VerifySecureDigestRequest request) { + byte[] providedSignature = request.getDigest(); + Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty."); + @SuppressWarnings({"unchecked", "rawtypes"}) byte[] computedSignature = digest((SecureRequest) request); + return MessageDigest.isEqual(providedSignature, computedSignature); + } + + protected boolean doVerify(VerifySecureDigestRequest request) throws Exception { + return messageDigest(request); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java new file mode 100644 index 00000000..a5ce3b04 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2018 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.VerifySecureDigestRequest; + +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.text.MessageFormat; + +abstract class AbstractSignatureAlgorithm extends AbstractSecureDigestAlgorithm + implements SignatureAlgorithm { + + private static final String KEY_TYPE_MSG_PATTERN = + "{0} {1} keys must be {2}s (implement {3}). Provided key type: {4}."; + + protected AbstractSignatureAlgorithm(String id, String jcaName) { + super(id, jcaName); + } + + @Override + protected void validateKey(Key key, boolean signing) { + // https://github.com/jwtk/jjwt/issues/68: + Class type = signing ? PrivateKey.class : PublicKey.class; + if (!type.isInstance(key)) { + String msg = MessageFormat.format(KEY_TYPE_MSG_PATTERN, getId(), + keyType(signing), type.getSimpleName(), type.getName(), key.getClass().getName()); + throw new InvalidKeyException(msg); + } + } + + @Override + protected byte[] doDigest(final SecureRequest request) { + return jca(request).withSignature(new CheckedFunction() { + @Override + public byte[] apply(Signature sig) throws Exception { + sig.initSign(request.getKey()); + sig.update(request.getPayload()); + return sig.sign(); + } + }); + } + + @Override + protected boolean doVerify(final VerifySecureDigestRequest request) { + return jca(request).withSignature(new CheckedFunction() { + @Override + public Boolean apply(Signature sig) throws Exception { + sig.initVerify(request.getKey()); + sig.update(request.getPayload()); + return sig.verify(request.getDigest()); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java new file mode 100644 index 00000000..c3301189 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -0,0 +1,171 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AssociatedDataSupplier; +import io.jsonwebtoken.security.InitializationVectorSupplier; +import io.jsonwebtoken.security.KeyBuilderSupplier; +import io.jsonwebtoken.security.KeyLengthSupplier; +import io.jsonwebtoken.security.Request; +import io.jsonwebtoken.security.SecretKeyBuilder; +import io.jsonwebtoken.security.WeakKeyException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplier, KeyLengthSupplier { + + protected static final String KEY_ALG_NAME = "AES"; + protected static final int BLOCK_SIZE = 128; + protected static final int BLOCK_BYTE_SIZE = BLOCK_SIZE / Byte.SIZE; + protected static final int GCM_IV_SIZE = 96; // https://tools.ietf.org/html/rfc7518#section-5.3 + //protected static final int GCM_IV_BYTE_SIZE = GCM_IV_SIZE / Byte.SIZE; + protected static final String DECRYPT_NO_IV = "This algorithm implementation rejects decryption " + + "requests that do not include initialization vectors. AES ciphertext without an IV is weak and " + + "susceptible to attack."; + + protected final int keyBitLength; + protected final int ivBitLength; + protected final int tagBitLength; + protected final boolean gcm; + + AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) { + super(id, jcaTransformation); + Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256."); + this.keyBitLength = keyBitLength; + this.gcm = jcaTransformation.startsWith("AES/GCM"); + this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE); + // https://tools.ietf.org/html/rfc7518#section-5.2.3 through https://tools.ietf.org/html/rfc7518#section-5.3 : + this.tagBitLength = this.gcm ? BLOCK_SIZE : this.keyBitLength; + + // GCM mode only available on JDK 8 and later, so enable BC as a backup provider if necessary for <= JDK 7: + // TODO: remove when dropping JDK 7: + if (this.gcm) { + setProvider(Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + public Cipher get() throws Exception { + return Cipher.getInstance(jcaTransformation); + } + }))); + } + } + + @Override + public int getKeyBitLength() { + return this.keyBitLength; + } + + @Override + public SecretKeyBuilder keyBuilder() { + return new DefaultSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength()); + } + + protected SecretKey assertKey(SecretKey key) { + Assert.notNull(key, "Request key cannot be null."); + validateLengthIfPossible(key); + return key; + } + + private void validateLengthIfPossible(SecretKey key) { + validateLength(key, this.keyBitLength, false); + } + + protected static String lengthMsg(String id, String type, int requiredLengthInBits, long actualLengthInBits) { + return "The '" + id + "' algorithm requires " + type + " with a length of " + + Bytes.bitsMsg(requiredLengthInBits) + ". The provided key has a length of " + + Bytes.bitsMsg(actualLengthInBits) + "."; + } + + protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean propagate) { + byte[] keyBytes; + + try { + keyBytes = key.getEncoded(); + } catch (RuntimeException re) { + if (propagate) { + throw re; + } + //can't get the bytes to validate, e.g. hardware security module or later Android, so just return: + return null; + } + long keyBitLength = Bytes.bitLength(keyBytes); + if (keyBitLength < requiredBitLength) { + throw new WeakKeyException(lengthMsg(getId(), "keys", requiredBitLength, keyBitLength)); + } + + return keyBytes; + } + + protected byte[] assertBytes(byte[] bytes, String type, int requiredBitLen) { + long bitLen = Bytes.bitLength(bytes); + if (requiredBitLen != bitLen) { + String msg = lengthMsg(getId(), type, requiredBitLen, bitLen); + throw new IllegalArgumentException(msg); + } + return bytes; + } + + byte[] assertIvLength(final byte[] iv) { + return assertBytes(iv, "initialization vectors", this.ivBitLength); + } + + byte[] assertTag(byte[] tag) { + return assertBytes(tag, "authentication tags", this.tagBitLength); + } + + byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgumentException { + byte[] iv = src.getInitializationVector(); + Assert.notEmpty(iv, DECRYPT_NO_IV); + return assertIvLength(iv); + } + + protected byte[] ensureInitializationVector(Request request) { + byte[] iv = null; + if (request instanceof InitializationVectorSupplier) { + iv = Arrays.clean(((InitializationVectorSupplier) request).getInitializationVector()); + } + int ivByteLength = this.ivBitLength / Byte.SIZE; + if (iv == null || iv.length == 0) { + iv = new byte[ivByteLength]; + SecureRandom random = ensureSecureRandom(request); + random.nextBytes(iv); + } else { + assertIvLength(iv); + } + return iv; + } + + protected AlgorithmParameterSpec getIvSpec(byte[] iv) { + Assert.notEmpty(iv, "Initialization Vector byte array cannot be null or empty."); + return this.gcm ? new GCMParameterSpec(BLOCK_SIZE, iv) : new IvParameterSpec(iv); + } + + protected byte[] getAAD(AssociatedDataSupplier request) { + return Arrays.clean(request.getAssociatedData()); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java new file mode 100644 index 00000000..ec87f81b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java @@ -0,0 +1,105 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.DefaultJweHeader; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecretKeyAlgorithm; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class AesGcmKeyAlgorithm extends AesAlgorithm implements SecretKeyAlgorithm { + + public static final String TRANSFORMATION = "AES/GCM/NoPadding"; + + public AesGcmKeyAlgorithm(int keyLen) { + super("A" + keyLen + "GCMKW", TRANSFORMATION, keyLen); + } + + @Override + public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { + + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request.getPayload()); + final SecretKey cek = generateKey(request); + final byte[] iv = ensureInitializationVector(request); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + byte[] taggedCiphertext = jca(request).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.WRAP_MODE, kek, ivSpec); + return cipher.wrap(cek); + } + }); + + int tagByteLength = this.tagBitLength / Byte.SIZE; + // When using GCM mode, the JDK appends the authentication tag to the ciphertext, so let's extract it: + int ciphertextLength = taggedCiphertext.length - tagByteLength; + byte[] ciphertext = new byte[ciphertextLength]; + System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength); + byte[] tag = new byte[tagByteLength]; + System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, tagByteLength); + + String encodedIv = Encoders.BASE64URL.encode(iv); + String encodedTag = Encoders.BASE64URL.encode(tag); + request.getHeader().put(DefaultJweHeader.IV.getId(), encodedIv); + request.getHeader().put(DefaultJweHeader.TAG.getId(), encodedTag); + + return new DefaultKeyResult(cek, ciphertext); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request.getKey()); + final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Decryption request content (ciphertext) cannot be null or empty."); + final JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + final FieldReadable reader = new RequiredFieldReader(header); + final byte[] tag = reader.get(DefaultJweHeader.TAG); + final byte[] iv = reader.get(DefaultJweHeader.IV); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + //for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array: + final byte[] taggedCiphertext = Bytes.concat(cekBytes, tag); + + return jca(request).withCipher(new CheckedFunction() { + @Override + public SecretKey apply(Cipher cipher) throws Exception { + cipher.init(Cipher.UNWRAP_MODE, kek, ivSpec); + Key key = cipher.unwrap(taggedCiphertext, KEY_ALG_NAME, Cipher.SECRET_KEY); + Assert.state(key instanceof SecretKey, "cipher.unwrap must produce a SecretKey instance."); + return (SecretKey) key; + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java new file mode 100644 index 00000000..881af02c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java @@ -0,0 +1,74 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecretKeyAlgorithm; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.Key; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class AesWrapKeyAlgorithm extends AesAlgorithm implements SecretKeyAlgorithm { + + private static final String TRANSFORMATION = "AESWrap"; + + public AesWrapKeyAlgorithm(int keyLen) { + super("A" + keyLen + "KW", TRANSFORMATION, keyLen); + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request.getPayload()); + final SecretKey cek = generateKey(request); + + byte[] ciphertext = jca(request).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.WRAP_MODE, kek); + return cipher.wrap(cek); + } + }); + + return new DefaultKeyResult(cek, ciphertext); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final SecretKey kek = assertKey(request.getKey()); + final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request content (encrypted key) cannot be null or empty."); + + return jca(request).withCipher(new CheckedFunction() { + @Override + public SecretKey apply(Cipher cipher) throws Exception { + cipher.init(Cipher.UNWRAP_MODE, kek); + Key key = cipher.unwrap(cekBytes, KEY_ALG_NAME, Cipher.SECRET_KEY); + Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance."); + return (SecretKey) key; + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java new file mode 100644 index 00000000..76f4990b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AsymmetricJwkFactory.java @@ -0,0 +1,67 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; + +import java.security.Key; + +class AsymmetricJwkFactory implements FamilyJwkFactory> { + + private final String id; + private final FamilyJwkFactory> publicFactory; + private final FamilyJwkFactory> privateFactory; + + @SuppressWarnings({"unchecked", "rawtypes"}) + AsymmetricJwkFactory(FamilyJwkFactory publicFactory, FamilyJwkFactory privateFactory) { + this.publicFactory = (FamilyJwkFactory>) Assert.notNull(publicFactory, "publicFactory cannot be null."); + this.privateFactory = (FamilyJwkFactory>) Assert.notNull(privateFactory, "privateFactory cannot be null."); + this.id = Assert.notNull(publicFactory.getId(), "publicFactory id cannot be null or empty."); + Assert.isTrue(this.id.equals(privateFactory.getId()), "privateFactory id must equal publicFactory id"); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public boolean supports(JwkContext ctx) { + return ctx != null && + (this.id.equals(ctx.getType()) || privateFactory.supports(ctx) || publicFactory.supports(ctx)); + } + + @Override + public boolean supports(Key key) { + return key != null && (privateFactory.supports(key) || publicFactory.supports(key)); + } + + @Override + public JwkContext newContext(JwkContext src, Key key) { + return (privateFactory.supports(key) || privateFactory.supports(src)) ? + privateFactory.newContext(src, key) : + publicFactory.newContext(src, key); + } + + @Override + public Jwk createJwk(JwkContext ctx) { + if (privateFactory.supports(ctx)) { + return this.privateFactory.createJwk(ctx); + } + return this.publicFactory.createJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java new file mode 100644 index 00000000..5bd2f8ef --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -0,0 +1,156 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.Key; +import java.security.MessageDigest; + +import static io.jsonwebtoken.impl.lang.Bytes.*; + +/** + * 'Clean room' implementation of the Concat KDF algorithm based solely on + * NIST.800-56A, + * Section 5.8.1.1. Call the {@link #deriveKey(byte[], long, byte[]) deriveKey} method. + */ +final class ConcatKDF extends CryptoAlgorithm { + + private static final long MAX_REP_COUNT = 0xFFFFFFFFL; + private static final long MAX_HASH_INPUT_BYTE_LENGTH = Integer.MAX_VALUE; //no Java byte arrays bigger than this + private static final long MAX_HASH_INPUT_BIT_LENGTH = MAX_HASH_INPUT_BYTE_LENGTH * Byte.SIZE; + + private final int hashBitLength; + + /** + * NIST.SP.800-56Ar2.pdf, Section 5.8.1.1, Input requirement #2 says that the maximum bit length of the + * derived key cannot be more than this: + *
    +     *     hashBitLength * (2^32 - 1)
    +     * 
    + * However, this number is always greater than Integer.MAX_VALUE * Byte.SIZE, which is the maximum number of + * bits that can be represented in a Java byte array. So our implementation must be limited to that size + * regardless of what the spec allows: + */ + private static final long MAX_DERIVED_KEY_BIT_LENGTH = (long) Integer.MAX_VALUE * (long) Byte.SIZE; + + ConcatKDF(String jcaName) { + super("ConcatKDF", jcaName); + int hashByteLength = jca().withMessageDigest(new CheckedFunction() { + @Override + public Integer apply(MessageDigest instance) { + return instance.getDigestLength(); + } + }); + this.hashBitLength = hashByteLength * Byte.SIZE; + Assert.state(this.hashBitLength > 0, "MessageDigest length must be a positive value."); + } + + /** + * 'Clean room' implementation of the Concat KDF algorithm based solely on + * NIST.800-56A, + * Section 5.8.1.1. + * + * @param Z shared secret key to use to seed the derived secret. Cannot be null or empty. + * @param derivedKeyBitLength the total number of bits (not bytes) required in the returned derived + * key. + * @param otherInfo any additional party info to be associated with the derived key. May be null/empty. + * @return the derived key + * @throws UnsupportedKeyException if unable to obtain {@code sharedSecretKey}'s + * {@link Key#getEncoded() encoded byte array}. + * @throws SecurityException if unable to perform the necessary {@link MessageDigest} computations to + * generate the derived key. + */ + public SecretKey deriveKey(final byte[] Z, final long derivedKeyBitLength, final byte[] otherInfo) + throws UnsupportedKeyException, SecurityException { + + // sharedSecretKey argument assertions: + Assert.notEmpty(Z, "Z cannot be null or empty."); + + // derivedKeyBitLength argument assertions: + Assert.isTrue(derivedKeyBitLength > 0, "derivedKeyBitLength must be a positive integer."); + if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) { + String msg = "derivedKeyBitLength may not exceed " + bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + + ". Specified size: " + bitsMsg(derivedKeyBitLength) + "."; + throw new IllegalArgumentException(msg); + } + final long derivedKeyByteLength = derivedKeyBitLength / Byte.SIZE; + + final byte[] OtherInfo = otherInfo == null ? EMPTY : otherInfo; + + // Section 5.8.1.1, Process step #1: + final double repsd = derivedKeyBitLength / (double) this.hashBitLength; + final long reps = (long) Math.ceil(repsd); + // If repsd didn't result in a whole number, the last derived key byte will be partially filled per + // Section 5.8.1.1, Process step #6: + final boolean kLastPartial = repsd != (double) reps; + + // Section 5.8.1.1, Process step #2: + Assert.state(reps <= MAX_REP_COUNT, "derivedKeyBitLength is too large."); + + // Section 5.8.1.1, Process step #3: + final byte[] counter = new byte[]{0, 0, 0, 1}; // same as 0x0001L, but no extra step to convert to byte[] + + // Section 5.8.1.1, Process step #4: + long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo); + Assert.state(inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH, "Hash input is too large."); + + byte[] derivedKeyBytes = jca().withMessageDigest(new CheckedFunction() { + @Override + public byte[] apply(MessageDigest md) throws Exception { + + final ByteArrayOutputStream stream = new ByteArrayOutputStream((int) derivedKeyByteLength); + + // Section 5.8.1.1, Process step #5. We depart from Java idioms here by starting iteration index at 1 + // (instead of 0) and continue to <= reps (instead of < reps) to match the NIST publication algorithm + // notation convention (so variables like Ki and kLast below match the NIST definitions). + for (long i = 1; i <= reps; i++) { + + // Section 5.8.1.1, Process step #5.1: + md.update(counter); + md.update(Z); + md.update(OtherInfo); + byte[] Ki = md.digest(); + + // Section 5.8.1.1, Process step #5.2: + increment(counter); + + // Section 5.8.1.1, Process step #6: + if (i == reps && kLastPartial) { + long leftmostBitLength = derivedKeyBitLength % hashBitLength; + int leftmostByteLength = (int) (leftmostBitLength / Byte.SIZE); + byte[] kLast = new byte[leftmostByteLength]; + System.arraycopy(Ki, 0, kLast, 0, kLast.length); + Ki = kLast; + } + + stream.write(Ki); + } + + // Section 5.8.1.1, Process step #7: + return stream.toByteArray(); + } + }); + + return new SecretKeySpec(derivedKeyBytes, AesAlgorithm.KEY_ALG_NAME); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java new file mode 100644 index 00000000..9378feb5 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConstantKeyLocator.java @@ -0,0 +1,63 @@ +/* + * 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.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.LocatorAdapter; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.impl.lang.Function; + +import java.security.Key; + +@SuppressWarnings("deprecation") +public class ConstantKeyLocator extends LocatorAdapter implements SigningKeyResolver, Function, Key> { + + private final Key jwsKey; + private final Key jweKey; + + public ConstantKeyLocator(Key jwsKey, Key jweKey) { + this.jwsKey = jwsKey; + this.jweKey = jweKey; + } + + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + return locate(header); + } + + @Override + public Key resolveSigningKey(JwsHeader header, byte[] content) { + return locate(header); + } + + @Override + protected Key locate(JwsHeader header) { + return this.jwsKey; + } + + @Override + protected Key locate(JweHeader header) { + return this.jweKey; + } + + @Override + public Key apply(Header header) { + return locate(header); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java new file mode 100644 index 00000000..087fa8d3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -0,0 +1,124 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.Request; +import io.jsonwebtoken.security.SecretKeyBuilder; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +abstract class CryptoAlgorithm implements Identifiable { + + private final String ID; + + private final String jcaName; + + private Provider provider; // default, if any + + CryptoAlgorithm(String id, String jcaName) { + Assert.hasText(id, "id cannot be null or empty."); + this.ID = id; + Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.jcaName = jcaName; + } + + @Override + public String getId() { + return this.ID; + } + + String getJcaName() { + return this.jcaName; + } + + protected void setProvider(Provider provider) { // can be null + this.provider = provider; + } + + protected Provider getProvider() { + return this.provider; + } + + SecureRandom ensureSecureRandom(Request request) { + SecureRandom random = request != null ? request.getSecureRandom() : null; + return random != null ? random : Randoms.secureRandom(); + } + + protected JcaTemplate jca() { + return new JcaTemplate(getJcaName(), getProvider()); + } + + protected JcaTemplate jca(Request request) { + Assert.notNull(request, "request cannot be null."); + String jcaName = Assert.hasText(getJcaName(request), "Request jcaName cannot be null or empty."); + Provider provider = getProvider(request); + SecureRandom random = ensureSecureRandom(request); + return new JcaTemplate(jcaName, provider, random); + } + + protected String getJcaName(Request request) { + return getJcaName(); + } + + protected Provider getProvider(Request request) { + Provider provider = request.getProvider(); + if (provider == null) { + provider = this.provider; // fallback, if any + } + return provider; + } + + protected SecretKey generateKey(KeyRequest request) { + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + SecretKeyBuilder builder = Assert.notNull(enc.keyBuilder(), "Request encryptionAlgorithm keyBuilder cannot be null."); + SecretKey key = builder.setProvider(getProvider(request)).setRandom(request.getSecureRandom()).build(); + return Assert.notNull(key, "Request encryptionAlgorithm SecretKeyBuilder cannot produce null keys."); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof CryptoAlgorithm) { + CryptoAlgorithm other = (CryptoAlgorithm) obj; + return this.ID.equals(other.getId()) && this.jcaName.equals(other.getJcaName()); + } + return false; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 31 * hash + ID.hashCode(); + hash = 31 * hash + jcaName.hashCode(); + return hash; + } + + @Override + public String toString() { + return ID; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Curve.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Curve.java new file mode 100644 index 00000000..8982bd19 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Curve.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.security.KeyPairBuilderSupplier; + +public interface Curve extends Identifiable, KeyPairBuilderSupplier { +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java new file mode 100644 index 00000000..3e3e7fb0 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.lang.DefaultRegistry; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.IdRegistry; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Registry; + +import java.security.spec.EllipticCurve; +import java.util.Collection; +import java.util.LinkedHashSet; + +public final class Curves { + public static final Curve P_256 = new ECCurve("P-256", "secp256r1"); // JDK standard + public static final Curve P_384 = new ECCurve("P-384", "secp384r1"); // JDK standard + public static final Curve P_521 = new ECCurve("P-521", "secp521r1"); // JDK standard + + private static final Collection EC_CURVES = Collections.setOf((ECCurve) P_256, (ECCurve) P_384, (ECCurve) P_521); + + private static final Collection VALUES = new LinkedHashSet<>(); + + static { + VALUES.addAll(EC_CURVES); + VALUES.addAll(EdwardsCurve.VALUES); + } + + private static final Registry CURVES_BY_ID = new IdRegistry<>("Elliptic Curve", VALUES); + private static final Registry CURVES_BY_JCA_NAME = new DefaultRegistry<>( + "Elliptic Curve", "JCA name", VALUES, new Function() { + @Override + public String apply(Curve curve) { + return ((DefaultCurve) curve).getJcaName(); + } + }); + + private static final Registry CURVES_BY_JCA_CURVE = new DefaultRegistry<>( + "Elliptic Curve", "ECCurve instance", EC_CURVES, new Function() { + @Override + public EllipticCurve apply(ECCurve curve) { + return curve.toParameterSpec().getCurve(); + } + }); + + //prevent instantiation + private Curves() { + } + + public static Curve findById(String jwaId) { + Assert.hasText(jwaId, "jwaId cannot be null or empty."); + return CURVES_BY_ID.find(jwaId); + } + + public static Curve findByJcaName(String jcaName) { + Assert.hasText(jcaName, "jcaName cannot be null or empty."); + return CURVES_BY_JCA_NAME.find(jcaName); + } + + public static ECCurve findBy(EllipticCurve curve) { + Assert.notNull(curve, "EllipticCurve argument cannot be null."); + return CURVES_BY_JCA_CURVE.find(curve); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java new file mode 100644 index 00000000..393a2eaa --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadRequest.java @@ -0,0 +1,55 @@ +/* + * 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.security; + +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.InitializationVectorSupplier; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultAeadRequest extends DefaultSecureRequest + implements AeadRequest, InitializationVectorSupplier { + + private final byte[] IV; + + private final byte[] AAD; + + DefaultAeadRequest(byte[] data, Provider provider, SecureRandom secureRandom, SecretKey key, byte[] aad, byte[] iv) { + super(data, provider, secureRandom, key); + this.AAD = aad; + this.IV = iv; + } + + public DefaultAeadRequest(byte[] data, Provider provider, SecureRandom secureRandom, SecretKey key, byte[] aad) { + this(data, provider, secureRandom, key, aad, null); + } + + public DefaultAeadRequest(byte[] data, SecretKey key, byte[] aad) { + this(data, null, null, key, aad, null); + } + + @Override + public byte[] getAssociatedData() { + return this.AAD; + } + + @Override + public byte[] getInitializationVector() { + return this.IV; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java new file mode 100644 index 00000000..f26a5aae --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java @@ -0,0 +1,40 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.DecryptAeadRequest; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultAeadResult extends DefaultAeadRequest implements AeadResult, DecryptAeadRequest { + + private final byte[] TAG; + + public DefaultAeadResult(Provider provider, SecureRandom secureRandom, byte[] data, SecretKey key, byte[] aad, byte[] tag, byte[] iv) { + super(data, provider, secureRandom, key, aad, iv); + Assert.notEmpty(iv, "initialization vector cannot be null or empty."); + this.TAG = Assert.notEmpty(tag, "authentication tag cannot be null or empty."); + } + + @Override + public byte[] getDigest() { + return this.TAG; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCurve.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCurve.java new file mode 100644 index 00000000..7598cf64 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCurve.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.KeyPairBuilder; + +import java.security.Provider; + +class DefaultCurve implements Curve { + + private final String ID; + + private final String JCA_NAME; + + private final Provider PROVIDER; // can be null + + DefaultCurve(String id, String jcaName) { + this(id, jcaName, null); + } + + DefaultCurve(String id, String jcaName, Provider provider) { + this.ID = Assert.notNull(Strings.clean(id), "Curve ID cannot be null or empty."); + this.JCA_NAME = Assert.notNull(Strings.clean(jcaName), "Curve jcaName cannot be null or empty."); + this.PROVIDER = provider; + } + + @Override + public String getId() { + return this.ID; + } + + public String getJcaName() { + return this.JCA_NAME; + } + + public Provider getProvider() { + return this.PROVIDER; + } + + @Override + public int hashCode() { + return ID.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Curve) { + Curve curve = (Curve) obj; + return ID.equals(curve.getId()); + } + return false; + } + + @Override + public String toString() { + return ID; + } + + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder(this.JCA_NAME).setProvider(this.PROVIDER); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java new file mode 100644 index 00000000..9bdc6a77 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultDecryptionKeyRequest.java @@ -0,0 +1,45 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultDecryptionKeyRequest extends DefaultKeyRequest implements DecryptionKeyRequest { + + private final K decryptionKey; + + public DefaultDecryptionKeyRequest(byte[] encryptedCek, Provider provider, SecureRandom secureRandom, JweHeader header, AeadAlgorithm encryptionAlgorithm, K decryptionKey) { + super(encryptedCek, provider, secureRandom, header, encryptionAlgorithm); + this.decryptionKey = Assert.notNull(decryptionKey, "decryption key cannot be null."); + } + + @Override + protected void assertBytePayload(byte[] payload) { + Assert.notNull(payload, "encrypted key bytes cannot be null (but may be empty."); + } + + @Override + public K getKey() { + return this.decryptionKey; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java new file mode 100644 index 00000000..33a9a1c2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java @@ -0,0 +1,41 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; + +import java.math.BigInteger; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.util.Set; + +class DefaultEcPrivateJwk extends AbstractPrivateJwk implements EcPrivateJwk { + + static final Field D = Fields.secretBigInt("d", "ECC Private Key"); + static final Set> FIELDS = Collections.concat(DefaultEcPublicJwk.FIELDS, D); + + DefaultEcPrivateJwk(JwkContext ctx, EcPublicJwk pubJwk) { + super(ctx, + // only public members are included in Private JWK Thumbprints per + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + DefaultEcPublicJwk.THUMBPRINT_FIELDS, + pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java new file mode 100644 index 00000000..907b6659 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java @@ -0,0 +1,42 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.EcPublicJwk; + +import java.math.BigInteger; +import java.security.interfaces.ECPublicKey; +import java.util.List; +import java.util.Set; + +class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPublicJwk { + + static final String TYPE_VALUE = "EC"; + static final Field CRV = Fields.string("crv", "Curve"); + static final Field X = Fields.bigInt("x", "X Coordinate").build(); + static final Field Y = Fields.bigInt("y", "Y Coordinate").build(); + static final Set> FIELDS = Collections.concat(AbstractAsymmetricJwk.FIELDS, CRV, X, Y); + + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2 + static final List> THUMBPRINT_FIELDS = Collections.>of(CRV, KTY, X, Y); + + DefaultEcPublicJwk(JwkContext ctx) { + super(ctx, THUMBPRINT_FIELDS); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java new file mode 100644 index 00000000..1d61cc37 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultHashAlgorithm.java @@ -0,0 +1,60 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.HashAlgorithm; +import io.jsonwebtoken.security.Request; +import io.jsonwebtoken.security.VerifyDigestRequest; + +import java.security.MessageDigest; +import java.security.Provider; +import java.util.Locale; + +public final class DefaultHashAlgorithm extends CryptoAlgorithm implements HashAlgorithm { + + public static final HashAlgorithm SHA1 = new DefaultHashAlgorithm("sha-1"); + + DefaultHashAlgorithm(String id) { + super(id, id.toUpperCase(Locale.ENGLISH)); + } + + DefaultHashAlgorithm(String id, String jcaName, Provider provider) { + super(id, jcaName); + setProvider(provider); + } + + @Override + public byte[] digest(final Request request) { + Assert.notNull(request, "Request cannot be null."); + final byte[] payload = Assert.notNull(request.getPayload(), "Request payload cannot be null."); + return jca(request).withMessageDigest(new CheckedFunction() { + @Override + public byte[] apply(MessageDigest md) { + return md.digest(payload); + } + }); + } + + @Override + public boolean verify(VerifyDigestRequest request) { + Assert.notNull(request, "VerifyDigestRequest cannot be null."); + byte[] digest = Assert.notNull(request.getDigest(), "Digest cannot be null."); + byte[] computed = digest(request); + return MessageDigest.isEqual(computed, digest); // time-constant comparison required, not standard equals + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java new file mode 100644 index 00000000..dc32ff68 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.JwtMap; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.HashAlgorithm; + +import java.net.URI; +import java.security.Key; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static io.jsonwebtoken.lang.Strings.nespace; + +public class DefaultJwkContext extends JwtMap implements JwkContext { + + private static final Set> DEFAULT_FIELDS; + + static { // assume all JWA fields: + Set> set = new LinkedHashSet<>(); + set.addAll(DefaultSecretJwk.FIELDS); // Private/Secret JWKs has both public and private fields + set.addAll(DefaultEcPrivateJwk.FIELDS); // Private JWKs have both public and private fields + set.addAll(DefaultRsaPrivateJwk.FIELDS); // Private JWKs have both public and private fields + set.addAll(DefaultOctetPrivateJwk.FIELDS); // Private JWKs have both public and private fields + + // EC JWKs and Octet JWKs have two fields that are named identically, but have different type requirements. So + // we swap out those fields with placeholders that allow either. When the JwkContext is converted to its + // type-specific context by the ProtoBuilder, the values will be correctly converted to their required types + // at that time. It is also important to retain toString security (via field.setSecret(true)) to ensure + // any printing of the builder or its internal context does not print secure data. + set.remove(DefaultEcPublicJwk.X); + set.remove(DefaultEcPrivateJwk.D); + set.add(Fields.string(DefaultEcPublicJwk.X.getId(), "Elliptic Curve public key X coordinate")); + set.add(Fields.builder(String.class).setSecret(true) + .setId(DefaultEcPrivateJwk.D.getId()).setName("Elliptic Curve private key").build()); + + DEFAULT_FIELDS = Collections.immutable(set); + } + + private K key; + private PublicKey publicKey; + private Provider provider; + + private SecureRandom random; + + private HashAlgorithm idThumbprintAlgorithm; + + public DefaultJwkContext() { + // For the default constructor case, we don't know how it will be used or what values will be populated, + // so we can't know ahead of time what the sensitive data is. As such, for security reasons, we assume all + // the known fields for all supported keys/algorithms in case it is used for any of them: + this(DEFAULT_FIELDS); + } + + public DefaultJwkContext(Set> fields) { + super(fields); + } + + public DefaultJwkContext(Set> fields, JwkContext other) { + this(fields, other, true); + } + + public DefaultJwkContext(Set> fields, JwkContext other, K key) { + //if the key is null or a PublicKey, we don't want to redact - we want to fully remove the items that are + //private names (public JWKs should never contain any private key fields, even if redacted): + this(fields, other, (key == null || key instanceof PublicKey)); + this.key = Assert.notNull(key, "Key cannot be null."); + } + + public DefaultJwkContext(Set> fields, JwkContext other, boolean removePrivate) { + super(Assert.notEmpty(fields, "Fields cannot be null or empty.")); + Assert.notNull(other, "JwkContext cannot be null."); + Assert.isInstanceOf(DefaultJwkContext.class, other, "JwkContext must be a DefaultJwkContext instance."); + DefaultJwkContext src = (DefaultJwkContext) other; + this.provider = other.getProvider(); + this.random = other.getRandom(); + this.idThumbprintAlgorithm = other.getIdThumbprintAlgorithm(); + this.values.putAll(src.values); + // Ensure the source's idiomatic values match the types expected by this object: + for (Map.Entry entry : src.idiomaticValues.entrySet()) { + String id = entry.getKey(); + Object value = entry.getValue(); + Field field = this.FIELDS.get(id); + if (field != null && !field.supports(value)) { // src idiomatic value is not what is expected, so convert: + value = this.values.get(field.getId()); + put(field, value); // perform idiomatic conversion with original/raw src value + } else { + this.idiomaticValues.put(id, value); + } + } + if (removePrivate) { + for (Field field : src.FIELDS.values()) { + if (field.isSecret()) { + remove(field.getId()); + } + } + } + } + + @Override + public String getName() { + String value = get(AbstractJwk.KTY); + if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { + value = "Secret"; + } else if (DefaultOctetPublicJwk.TYPE_VALUE.equals(value)) { + value = "Octet"; + } + StringBuilder sb = value != null ? new StringBuilder(value) : new StringBuilder(); + K key = getKey(); + if (key instanceof PublicKey) { + nespace(sb).append("Public"); + } else if (key instanceof PrivateKey) { + nespace(sb).append("Private"); + } + nespace(sb).append("JWK"); + return sb.toString(); + } + + @Override + public void putAll(Map m) { + Assert.notEmpty(m, "JWK values cannot be null or empty."); + super.putAll(m); + } + + @Override + public String getAlgorithm() { + return idiomaticGet(AbstractJwk.ALG); + } + + @Override + public JwkContext setAlgorithm(String algorithm) { + put(AbstractJwk.ALG, algorithm); + return this; + } + + @Override + public String getId() { + return idiomaticGet(AbstractJwk.KID); + } + + @Override + public JwkContext setId(String id) { + put(AbstractJwk.KID, id); + return this; + } + + @Override + public JwkContext setIdThumbprintAlgorithm(HashAlgorithm alg) { + this.idThumbprintAlgorithm = alg; + return this; + } + + @Override + public HashAlgorithm getIdThumbprintAlgorithm() { + return this.idThumbprintAlgorithm; + } + + @Override + public Set getOperations() { + return idiomaticGet(AbstractJwk.KEY_OPS); + } + + @Override + public JwkContext setOperations(Set ops) { + put(AbstractJwk.KEY_OPS, ops); + return this; + } + + @Override + public String getType() { + return idiomaticGet(AbstractJwk.KTY); + } + + @Override + public JwkContext setType(String type) { + put(AbstractJwk.KTY, type); + return this; + } + + @Override + public String getPublicKeyUse() { + return idiomaticGet(AbstractAsymmetricJwk.USE); + } + + @Override + public JwkContext setPublicKeyUse(String use) { + put(AbstractAsymmetricJwk.USE, use); + return this; + } + + @Override + public List getX509CertificateChain() { + return idiomaticGet(AbstractAsymmetricJwk.X5C); + } + + @Override + public JwkContext setX509CertificateChain(List x5c) { + put(AbstractAsymmetricJwk.X5C, x5c); + return this; + } + + @Override + public byte[] getX509CertificateSha1Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T); + } + + @Override + public JwkContext setX509CertificateSha1Thumbprint(byte[] x5t) { + put(AbstractAsymmetricJwk.X5T, x5t); + return this; + } + + @Override + public byte[] getX509CertificateSha256Thumbprint() { + return idiomaticGet(AbstractAsymmetricJwk.X5T_S256); + } + + @Override + public JwkContext setX509CertificateSha256Thumbprint(byte[] x5ts256) { + put(AbstractAsymmetricJwk.X5T_S256, x5ts256); + return this; + } + + @Override + public URI getX509Url() { + return idiomaticGet(AbstractAsymmetricJwk.X5U); + } + + @Override + public JwkContext setX509Url(URI url) { + put(AbstractAsymmetricJwk.X5U, url); + return this; + } + + @Override + public K getKey() { + return this.key; + } + + @Override + public JwkContext setKey(K key) { + this.key = key; + return this; + } + + @Override + public PublicKey getPublicKey() { + return this.publicKey; + } + + @Override + public JwkContext setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + @Override + public Provider getProvider() { + return this.provider; + } + + @Override + public JwkContext setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public SecureRandom getRandom() { + return this.random; + } + + @Override + public JwkContext setRandom(SecureRandom random) { + this.random = random; + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java new file mode 100644 index 00000000..1f4d3ecc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParser.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.JwkParser; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.nio.charset.StandardCharsets; +import java.security.Provider; +import java.util.Map; + +public class DefaultJwkParser implements JwkParser { + + private final Provider provider; + + private final Deserializer> deserializer; + + public DefaultJwkParser(Provider provider, Deserializer> deserializer) { + this.provider = provider; + this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); + } + + // visible for testing + protected Map deserialize(String json) { + byte[] data = json.getBytes(StandardCharsets.UTF_8); + return this.deserializer.deserialize(data); + } + + @Override + public Jwk parse(String json) throws KeyException { + Assert.hasText(json, "JSON string argument cannot be null or empty."); + Map data; + try { + data = deserialize(json); + } catch (Exception e) { + String msg = "Unable to deserialize JSON string argument: " + e.getMessage(); + throw new MalformedKeyException(msg); + } + + JwkBuilder builder = Jwks.builder(); + + if (this.provider != null) { + builder.setProvider(this.provider); + } + + return builder.putAll(data).build(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java new file mode 100644 index 00000000..a2ed3a81 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkParserBuilder.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.lang.Services; +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.security.JwkParser; +import io.jsonwebtoken.security.JwkParserBuilder; + +import java.security.Provider; +import java.util.Map; + +@SuppressWarnings("unused") //used via reflection by Jwks.parser() +public class DefaultJwkParserBuilder implements JwkParserBuilder { + + private Provider provider; + + private Deserializer> deserializer; + + @Override + public JwkParserBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public JwkParserBuilder deserializeJsonWith(Deserializer> deserializer) { + this.deserializer = deserializer; + return this; + } + + @Override + public JwkParser build() { + if (this.deserializer == null) { + // try to find one based on the services available: + //noinspection unchecked + this.deserializer = Services.loadFirst(Deserializer.class); + } + + return new DefaultJwkParser(this.provider, this.deserializer); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkThumbprint.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkThumbprint.java new file mode 100644 index 00000000..d18e8e41 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkThumbprint.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.HashAlgorithm; +import io.jsonwebtoken.security.JwkThumbprint; + +import java.net.URI; +import java.security.MessageDigest; + +class DefaultJwkThumbprint implements JwkThumbprint { + + private static final String URI_PREFIX = "urn:ietf:params:oauth:jwk-thumbprint:"; + + private final byte[] digest; + private final HashAlgorithm alg; + private final URI uri; + private final int hashcode; + private final String sval; + + DefaultJwkThumbprint(byte[] digest, HashAlgorithm alg) { + this.digest = Assert.notEmpty(digest, "Thumbprint digest byte array cannot be null or empty."); + this.alg = Assert.notNull(alg, "Thumbprint HashAlgorithm cannot be null."); + String id = Assert.hasText(Strings.clean(alg.getId()), "Thumbprint HashAlgorithm id cannot be null or empty."); + String base64Url = Encoders.BASE64URL.encode(digest); + String s = URI_PREFIX + id + ":" + base64Url; + this.uri = URI.create(s); + this.hashcode = Objects.nullSafeHashCode(this.digest, this.alg); + this.sval = Encoders.BASE64URL.encode(digest); + } + + @Override + public HashAlgorithm getHashAlgorithm() { + return this.alg; + } + + @Override + public byte[] toByteArray() { + return this.digest.clone(); + } + + @Override + public URI toURI() { + return this.uri; + } + + @Override + public String toString() { + return sval; + } + + @Override + public int hashCode() { + return this.hashcode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof DefaultJwkThumbprint) { + DefaultJwkThumbprint other = (DefaultJwkThumbprint) obj; + return this.alg.equals(other.alg) && + MessageDigest.isEqual(this.digest, other.digest); + } + + return false; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java new file mode 100644 index 00000000..3ab2e333 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPair.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPair; + +import java.security.PrivateKey; +import java.security.PublicKey; + +public class DefaultKeyPair implements KeyPair { + + private final A publicKey; + private final B privateKey; + + private final java.security.KeyPair jdkPair; + + public DefaultKeyPair(A publicKey, B privateKey) { + this.publicKey = Assert.notNull(publicKey, "PublicKey argument cannot be null."); + this.privateKey = Assert.notNull(privateKey, "PrivateKey argument cannot be null."); + this.jdkPair = new java.security.KeyPair(this.publicKey, this.privateKey); + } + + @Override + public A getPublic() { + return this.publicKey; + } + + @Override + public B getPrivate() { + return this.privateKey; + } + + @Override + public java.security.KeyPair toJavaKeyPair() { + return this.jdkPair; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java new file mode 100644 index 00000000..647d3108 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPairBuilder; + +import java.security.KeyPair; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +public class DefaultKeyPairBuilder implements KeyPairBuilder { + + private final String jcaName; + private final int bitLength; + private final AlgorithmParameterSpec params; + private Provider provider; + private SecureRandom random; + + public DefaultKeyPairBuilder(String jcaName) { + this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.bitLength = 0; + this.params = null; + } + + public DefaultKeyPairBuilder(String jcaName, int bitLength) { + this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.bitLength = Assert.gt(bitLength, 0, "bitLength must be a positive integer greater than 0"); + this.params = null; + } + + public DefaultKeyPairBuilder(String jcaName, AlgorithmParameterSpec params) { + this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + this.params = Assert.notNull(params, "AlgorithmParameterSpec params cannot be null."); + this.bitLength = 0; + } + + @Override + public KeyPair build() { + JcaTemplate template = new JcaTemplate(this.jcaName, this.provider, this.random); + if (this.params != null) { + return template.generateKeyPair(this.params); + } else if (this.bitLength > 0) { + return template.generateKeyPair(this.bitLength); + } else { + return template.generateKeyPair(); + } + } + + @Override + public KeyPairBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public KeyPairBuilder setRandom(SecureRandom random) { + this.random = random; + return this; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java new file mode 100644 index 00000000..c91b3e06 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.KeyRequest; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultKeyRequest extends DefaultRequest implements KeyRequest { + + private final JweHeader header; + private final AeadAlgorithm encryptionAlgorithm; + + public DefaultKeyRequest(T payload, Provider provider, SecureRandom secureRandom, JweHeader header, AeadAlgorithm encryptionAlgorithm) { + super(payload, provider, secureRandom); + this.header = Assert.notNull(header, "JweHeader cannot be null."); + this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "AeadAlgorithm argument cannot be null."); + } + + @Override + public JweHeader getHeader() { + return this.header; + } + + @Override + public AeadAlgorithm getEncryptionAlgorithm() { + return this.encryptionAlgorithm; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java new file mode 100644 index 00000000..abda9550 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyResult; + +import javax.crypto.SecretKey; + +public class DefaultKeyResult extends DefaultMessage implements KeyResult { + + private final SecretKey key; + + public DefaultKeyResult(SecretKey key) { + this(key, Bytes.EMPTY); + } + + public DefaultKeyResult(SecretKey key, byte[] encryptedKey) { + super(encryptedKey); + this.key = Assert.notNull(key, "Content Encryption Key cannot be null."); + } + + @Override + protected void assertBytePayload(byte[] payload) { + Assert.notNull(payload, "encrypted key bytes cannot be null (but may be empty."); + } + + @Override + public SecretKey getKey() { + return this.key; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java new file mode 100644 index 00000000..2da21b6e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyUseStrategy.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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.security; + +public class DefaultKeyUseStrategy implements KeyUseStrategy { + + static final KeyUseStrategy INSTANCE = new DefaultKeyUseStrategy(); + + // values from https://www.rfc-editor.org/rfc/rfc7517.html#section-4.2 + private static final String SIGNATURE = "sig"; + private static final String ENCRYPTION = "enc"; + + @Override + public String toJwkValue(KeyUsage usage) { + + // states 2, 3, 4 + if (usage.isKeyEncipherment() || usage.isDataEncipherment() || usage.isKeyAgreement()) { + return ENCRYPTION; + } + + // states 0, 1, 5, 6 + if (usage.isDigitalSignature() || usage.isNonRepudiation() || usage.isKeyCertSign() || usage.isCRLSign()) { + return SIGNATURE; + } + + // We don't need to check for encipherOnly (7) and decipherOnly (8) because per + // [RFC 5280, Section 4.2.1.3](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3), + // those two states are only relevant when keyAgreement (4) is true, and that is covered in the first + // conditional above + + return null; //can't infer + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java new file mode 100644 index 00000000..b4585042 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java @@ -0,0 +1,166 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.MacAlgorithm; +import io.jsonwebtoken.security.SecretKeyBuilder; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.WeakKeyException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import java.security.Key; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultMacAlgorithm extends AbstractSecureDigestAlgorithm implements MacAlgorithm { + + private final int minKeyBitLength; //in bits + + private static final Set JWA_STANDARD_IDS = new LinkedHashSet<>(Collections.of("HS256", "HS384", "HS512")); + + // PKCS12 OIDs are added to these lists per https://bugs.openjdk.java.net/browse/JDK-8243551 + private static final Set HS256_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA256", "1.2.840.113549.2.9")); + private static final Set HS384_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA384", "1.2.840.113549.2.10")); + private static final Set HS512_JCA_NAMES = new LinkedHashSet<>(Collections.of("HMACSHA512", "1.2.840.113549.2.11")); + + private static final Set VALID_HS256_JCA_NAMES; + private static final Set VALID_HS384_JCA_NAMES; + + static { + VALID_HS384_JCA_NAMES = new LinkedHashSet<>(HS384_JCA_NAMES); + VALID_HS384_JCA_NAMES.addAll(HS512_JCA_NAMES); + VALID_HS256_JCA_NAMES = new LinkedHashSet<>(HS256_JCA_NAMES); + VALID_HS256_JCA_NAMES.addAll(VALID_HS384_JCA_NAMES); + } + + public DefaultMacAlgorithm(int digestBitLength) { + this("HS" + digestBitLength, "HmacSHA" + digestBitLength, digestBitLength); + } + + public DefaultMacAlgorithm(String id, String jcaName, int minKeyBitLength) { + super(id, jcaName); + Assert.isTrue(minKeyBitLength > 0, "minKeyLength must be greater than zero."); + this.minKeyBitLength = minKeyBitLength; + } + + @Override + public int getKeyBitLength() { + return this.minKeyBitLength; + } + + private boolean isJwaStandard() { + return JWA_STANDARD_IDS.contains(getId()); + } + + private boolean isJwaStandardJcaName(String jcaName) { + return VALID_HS256_JCA_NAMES.contains(jcaName.toUpperCase(Locale.ENGLISH)); + } + + @Override + public SecretKeyBuilder keyBuilder() { + return new DefaultSecretKeyBuilder(getJcaName(), getKeyBitLength()); + } + + @Override + protected void validateKey(Key k, boolean signing) { + + final String keyType = keyType(signing); + + if (k == null) { + throw new IllegalArgumentException("Signature " + keyType + " key cannot be null."); + } + + if (!(k instanceof SecretKey)) { + String msg = "MAC " + keyType(signing) + " keys must be SecretKey instances. Specified key is of type " + + k.getClass().getName(); + throw new InvalidKeyException(msg); + } + + final SecretKey key = (SecretKey) k; + + final String id = getId(); + + String alg = key.getAlgorithm(); + if (!Strings.hasText(alg)) { + String msg = "The " + keyType + " key's algorithm cannot be null or empty."; + throw new InvalidKeyException(msg); + } + + //assert key's jca name is valid if it's a JWA standard algorithm: + if (isJwaStandard() && !isJwaStandardJcaName(alg)) { + throw new InvalidKeyException("The " + keyType + " key's algorithm '" + alg + "' does not equal a valid " + + "HmacSHA* algorithm name or PKCS12 OID and cannot be used with " + id + "."); + } + + byte[] encoded = null; + + // https://github.com/jwtk/jjwt/issues/478 + // + // Some KeyStore implementations (like Hardware Security Modules and later versions of Android) will not allow + // applications or libraries to obtain the secret key's encoded bytes. In these cases, key length assertions + // cannot be made, so we'll need to skip the key length checks if so. + try { + encoded = key.getEncoded(); + } catch (Exception ignored) { + } + + // We can only perform length validation if key.getEncoded() is not null or does not throw an exception + // per https://github.com/jwtk/jjwt/issues/478 and https://github.com/jwtk/jjwt/issues/619 + // so return early if we can't: + if (encoded == null) return; + + int size = (int) Bytes.bitLength(encoded); + if (size < this.minKeyBitLength) { + String msg = "The " + keyType + " key's size is " + size + " bits which " + + "is not secure enough for the " + id + " algorithm."; + + if (isJwaStandard() && isJwaStandardJcaName(getJcaName())) { //JWA standard algorithm name - reference the spec: + msg += " The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + id + " MUST have a " + + "size >= " + minKeyBitLength + " bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the StandardSecureDigestAlgorithms." + id + ".keyBuilder() " + + "method to create a key guaranteed to be secure enough for " + id + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + } else { //custom algorithm - just indicate required key length: + msg += " The " + id + " algorithm requires keys to have a size >= " + minKeyBitLength + " bits."; + } + + throw new WeakKeyException(msg); + } + } + + @Override + public byte[] doDigest(final SecureRequest request) { + return jca(request).withMac(new CheckedFunction() { + @Override + public byte[] apply(Mac mac) throws Exception { + mac.init(request.getKey()); + return mac.doFinal(request.getPayload()); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java new file mode 100644 index 00000000..7a777ff1 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMessage.java @@ -0,0 +1,39 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Message; + +class DefaultMessage implements Message { + + private final T payload; + + DefaultMessage(T payload) { + this.payload = Assert.notNull(payload, "payload cannot be null."); + if (payload instanceof byte[]) { + assertBytePayload((byte[])payload); + } + } + protected void assertBytePayload(byte[] payload) { + Assert.notEmpty(payload, "payload byte array cannot be null or empty."); + } + + @Override + public T getPayload() { + return payload; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPrivateJwk.java new file mode 100644 index 00000000..d5ded87d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPrivateJwk.java @@ -0,0 +1,40 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.OctetPrivateJwk; +import io.jsonwebtoken.security.OctetPublicJwk; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Set; + +public class DefaultOctetPrivateJwk extends AbstractPrivateJwk> implements OctetPrivateJwk { + + static final Field D = Fields.bytes("d", "The private key").setSecret(true).build(); + + static final Set> FIELDS = Collections.concat(DefaultOctetPublicJwk.FIELDS, D); + + DefaultOctetPrivateJwk(JwkContext ctx, OctetPublicJwk

    pubJwk) { + super(ctx, + // only public members are included in Private JWK Thumbprints per + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + DefaultOctetPublicJwk.THUMBPRINT_FIELDS, pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java new file mode 100644 index 00000000..4982d3ae --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java @@ -0,0 +1,40 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.OctetPublicJwk; + +import java.security.PublicKey; +import java.util.List; +import java.util.Set; + +public class DefaultOctetPublicJwk extends AbstractPublicJwk implements OctetPublicJwk { + + static final String TYPE_VALUE = "OKP"; + static final Field CRV = DefaultEcPublicJwk.CRV; + static final Field X = Fields.bytes("x", "The public key").build(); + static final Set> FIELDS = Collections.concat(AbstractAsymmetricJwk.FIELDS, CRV, X); + + // https://www.rfc-editor.org/rfc/rfc8037#section-2 (last paragraph): + static final List> THUMBPRINT_FIELDS = Collections.>of(CRV, KTY, X); + + DefaultOctetPublicJwk(JwkContext ctx) { + super(ctx, THUMBPRINT_FIELDS); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java new file mode 100644 index 00000000..ae86291e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultProtoJwkBuilder.java @@ -0,0 +1,209 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.EcPrivateJwkBuilder; +import io.jsonwebtoken.security.EcPublicJwkBuilder; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.OctetPrivateJwkBuilder; +import io.jsonwebtoken.security.OctetPublicJwkBuilder; +import io.jsonwebtoken.security.PrivateJwkBuilder; +import io.jsonwebtoken.security.ProtoJwkBuilder; +import io.jsonwebtoken.security.PublicJwkBuilder; +import io.jsonwebtoken.security.RsaPrivateJwkBuilder; +import io.jsonwebtoken.security.RsaPublicJwkBuilder; +import io.jsonwebtoken.security.SecretJwkBuilder; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.List; + +@SuppressWarnings("unused") //used via reflection by io.jsonwebtoken.security.Jwks +public class DefaultProtoJwkBuilder> + extends AbstractJwkBuilder> implements ProtoJwkBuilder { + + public DefaultProtoJwkBuilder() { + super(new DefaultJwkContext()); + } + + @Override + public SecretJwkBuilder forKey(SecretKey key) { + return new AbstractJwkBuilder.DefaultSecretJwkBuilder(newContext(key)); + } + + @Override + public RsaPublicJwkBuilder forKey(RSAPublicKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultRsaPublicJwkBuilder(newContext(key)); + } + + @Override + public RsaPrivateJwkBuilder forKey(RSAPrivateKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultRsaPrivateJwkBuilder(newContext(key)); + } + + @Override + public EcPublicJwkBuilder forKey(ECPublicKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultEcPublicJwkBuilder(newContext(key)); + } + + @Override + public EcPrivateJwkBuilder forKey(ECPrivateKey key) { + return new AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder(newContext(key)); + } + + private static UnsupportedKeyException unsupportedKey(Key key, Exception e) { + String msg = "There is no builder that supports specified key of type " + + key.getClass().getName() + " with algorithm '" + key.getAlgorithm() + "'."; + return new UnsupportedKeyException(msg, e); + } + + @SuppressWarnings("unchecked") + @Override + public PublicJwkBuilder forKey(A key) { + if (key instanceof RSAPublicKey) { + return (PublicJwkBuilder) forKey((RSAPublicKey) key); + } else if (key instanceof ECPublicKey) { + return (PublicJwkBuilder) forKey((ECPublicKey) key); + } else { + try { + return forOctetKey(key); + } catch (Exception e) { + throw unsupportedKey(key, e); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public PrivateJwkBuilder forKey(B key) { + Assert.notNull(key, "Key cannot be null."); + if (key instanceof RSAPrivateKey) { + return (PrivateJwkBuilder) forKey((RSAPrivateKey) key); + } else if (key instanceof ECPrivateKey) { + return (PrivateJwkBuilder) forKey((ECPrivateKey) key); + } else { + try { + return forOctetKey(key); + } catch (Exception e) { + throw unsupportedKey(key, e); + } + } + } + + @Override + public OctetPublicJwkBuilder forOctetKey(A key) { + return new AbstractAsymmetricJwkBuilder.DefaultOctetPublicJwkBuilder<>(newContext(key)); + } + + @Override + public OctetPrivateJwkBuilder forOctetKey(A key) { + return new AbstractAsymmetricJwkBuilder.DefaultOctetPrivateJwkBuilder<>(newContext(key)); + } + + @Override + public RsaPublicJwkBuilder forRsaChain(X509Certificate... chain) { + Assert.notEmpty(chain, "chain cannot be null or empty."); + return forRsaChain(Arrays.asList(chain)); + } + + @Override + public RsaPublicJwkBuilder forRsaChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); + X509Certificate cert = chain.get(0); + PublicKey key = cert.getPublicKey(); + RSAPublicKey pubKey = KeyPairs.assertKey(key, RSAPublicKey.class, "The first X509Certificate's "); + return forKey(pubKey).setX509CertificateChain(chain); + } + + @Override + public EcPublicJwkBuilder forEcChain(X509Certificate... chain) { + Assert.notEmpty(chain, "chain cannot be null or empty."); + return forEcChain(Arrays.asList(chain)); + } + + @Override + public EcPublicJwkBuilder forEcChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); + X509Certificate cert = chain.get(0); + PublicKey key = cert.getPublicKey(); + ECPublicKey pubKey = KeyPairs.assertKey(key, ECPublicKey.class, "The first X509Certificate's "); + return forKey(pubKey).setX509CertificateChain(chain); + } + + @SuppressWarnings("unchecked") // ok because of the EdwardsCurve.assertEdwards calls + @Override + public OctetPrivateJwkBuilder forOctetKeyPair(KeyPair pair) { + PublicKey pub = KeyPairs.getKey(pair, PublicKey.class); + PrivateKey priv = KeyPairs.getKey(pair, PrivateKey.class); + EdwardsCurve.assertEdwards(pub); + EdwardsCurve.assertEdwards(priv); + return (OctetPrivateJwkBuilder) forOctetKey(priv).setPublicKey(pub); + } + + @Override + public OctetPublicJwkBuilder forOctetChain(X509Certificate... chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be null or empty."); + return forOctetChain(Arrays.asList(chain)); + } + + @SuppressWarnings("unchecked") // ok because of the EdwardsCurve.assertEdwards calls + @Override + public OctetPublicJwkBuilder forOctetChain(List chain) { + Assert.notEmpty(chain, "X509Certificate chain cannot be empty."); + X509Certificate cert = chain.get(0); + PublicKey key = cert.getPublicKey(); + Assert.notNull(key, "The first X509Certificate's PublicKey cannot be null."); + EdwardsCurve.assertEdwards(key); + return this.forOctetKey((A) key).setX509CertificateChain(chain); + } + + @Override + public RsaPrivateJwkBuilder forRsaKeyPair(KeyPair pair) { + RSAPublicKey pub = KeyPairs.getKey(pair, RSAPublicKey.class); + RSAPrivateKey priv = KeyPairs.getKey(pair, RSAPrivateKey.class); + return forKey(priv).setPublicKey(pub); + } + + @Override + public EcPrivateJwkBuilder forEcKeyPair(KeyPair pair) { + ECPublicKey pub = KeyPairs.getKey(pair, ECPublicKey.class); + ECPrivateKey priv = KeyPairs.getKey(pair, ECPrivateKey.class); + return forKey(priv).setPublicKey(pub); + } + + @Override + public J build() { + if (Strings.hasText(this.jwkContext.get(AbstractJwk.KTY))) { + // Ensure we have a context that represents the configured kty value. Converting the existing context to + // the type-specific context will also perform any necessary field value type conversion / error checking + // this will also perform any necessary field value type conversions / error checking + setContext(this.jwkFactory.newContext(this.jwkContext, this.jwkContext.getKey())); + } + return super.build(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRequest.java new file mode 100644 index 00000000..4c7c07bb --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRequest.java @@ -0,0 +1,43 @@ +/* + * 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.security; + +import io.jsonwebtoken.security.Request; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultRequest extends DefaultMessage implements Request { + + private final Provider provider; + private final SecureRandom secureRandom; + + public DefaultRequest(T payload, Provider provider, SecureRandom secureRandom) { + super(payload); + this.provider = provider; + this.secureRandom = secureRandom; + } + + @Override + public Provider getProvider() { + return this.provider; + } + + @Override + public SecureRandom getSecureRandom() { + return this.secureRandom; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java new file mode 100644 index 00000000..a3ff12dc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java @@ -0,0 +1,120 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.WeakKeyException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.math.BigInteger; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAKey; +import java.security.spec.AlgorithmParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { + + private final AlgorithmParameterSpec SPEC; //can be null + + private static final int MIN_KEY_BIT_LENGTH = 2048; + + public DefaultRsaKeyAlgorithm(String id, String jcaTransformationString) { + this(id, jcaTransformationString, null); + } + + public DefaultRsaKeyAlgorithm(String id, String jcaTransformationString, AlgorithmParameterSpec spec) { + super(id, jcaTransformationString); + this.SPEC = spec; //can be null + } + + private static String keyType(boolean encryption) { + return encryption ? "encryption" : "decryption"; + } + + protected void validate(Key key, boolean encryption) { // true = encryption, false = decryption + // Some PKCS11 providers and HSMs won't expose the RSAKey interface, so we have to check to see if we can cast + // If so, we can provide additional safety checks: + if (key instanceof RSAKey) { + RSAKey rsaKey = (RSAKey) key; + BigInteger modulus = Assert.notNull(rsaKey.getModulus(), "RSAKey modulus cannot be null."); + int size = modulus.bitLength(); + if (size < MIN_KEY_BIT_LENGTH) { + String id = getId(); + String section = id.startsWith("RSA1") ? "4.2" : "4.3"; + String msg = "The RSA " + keyType(encryption) + " key's size (modulus) is " + size + + " bits which is not secure enough for the " + id + " algorithm. " + + "The JWT JWA Specification (RFC 7518, Section " + section + ") states that RSA keys MUST " + + "have a size >= " + MIN_KEY_BIT_LENGTH + " bits. See " + + "https://www.rfc-editor.org/rfc/rfc7518.html#section-" + section + " for more information."; + throw new WeakKeyException(msg); + } + } + } + + @Override + public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { + Assert.notNull(request, "Request cannot be null."); + final PublicKey kek = Assert.notNull(request.getPayload(), "RSA PublicKey encryption key cannot be null."); + validate(kek, true); + final SecretKey cek = generateKey(request); + + byte[] ciphertext = jca(request).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + if (SPEC == null) { + cipher.init(Cipher.WRAP_MODE, kek, ensureSecureRandom(request)); + } else { + cipher.init(Cipher.WRAP_MODE, kek, SPEC, ensureSecureRandom(request)); + } + return cipher.wrap(cek); + } + }); + + return new DefaultKeyResult(cek, ciphertext); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + final PrivateKey kek = Assert.notNull(request.getKey(), "RSA PrivateKey decryption key cannot be null."); + validate(kek, false); + final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request content (encrypted key) cannot be null or empty."); + + return jca(request).withCipher(new CheckedFunction() { + @Override + public SecretKey apply(Cipher cipher) throws Exception { + if (SPEC == null) { + cipher.init(Cipher.UNWRAP_MODE, kek); + } else { + cipher.init(Cipher.UNWRAP_MODE, kek, SPEC); + } + Key key = cipher.unwrap(cekBytes, AesAlgorithm.KEY_ALG_NAME, Cipher.SECRET_KEY); + return Assert.isInstanceOf(SecretKey.class, key, "Cipher unwrap must return a SecretKey instance."); + } + }); + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java new file mode 100644 index 00000000..384b4b20 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; + +import java.math.BigInteger; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAOtherPrimeInfo; +import java.util.List; +import java.util.Set; + +class DefaultRsaPrivateJwk extends AbstractPrivateJwk implements RsaPrivateJwk { + + static final Field PRIVATE_EXPONENT = Fields.secretBigInt("d", "Private Exponent"); + static final Field FIRST_PRIME = Fields.secretBigInt("p", "First Prime Factor"); + static final Field SECOND_PRIME = Fields.secretBigInt("q", "Second Prime Factor"); + static final Field FIRST_CRT_EXPONENT = Fields.secretBigInt("dp", "First Factor CRT Exponent"); + static final Field SECOND_CRT_EXPONENT = Fields.secretBigInt("dq", "Second Factor CRT Exponent"); + static final Field FIRST_CRT_COEFFICIENT = Fields.secretBigInt("qi", "First CRT Coefficient"); + static final Field> OTHER_PRIMES_INFO = + Fields.builder(RSAOtherPrimeInfo.class) + .setId("oth").setName("Other Primes Info") + .setConverter(RSAOtherPrimeInfoConverter.INSTANCE).list() + .build(); + + static final Set> FIELDS = Collections.concat(DefaultRsaPublicJwk.FIELDS, + PRIVATE_EXPONENT, FIRST_PRIME, SECOND_PRIME, FIRST_CRT_EXPONENT, + SECOND_CRT_EXPONENT, FIRST_CRT_COEFFICIENT, OTHER_PRIMES_INFO + ); + + DefaultRsaPrivateJwk(JwkContext ctx, RsaPublicJwk pubJwk) { + super(ctx, + // only public members are included in Private JWK Thumbprints per + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + DefaultRsaPublicJwk.THUMBPRINT_FIELDS, + pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java new file mode 100644 index 00000000..000c9e82 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2020 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.RsaPublicJwk; + +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; +import java.util.List; +import java.util.Set; + +class DefaultRsaPublicJwk extends AbstractPublicJwk implements RsaPublicJwk { + + static final String TYPE_VALUE = "RSA"; + static final Field MODULUS = Fields.bigInt("n", "Modulus").build(); + static final Field PUBLIC_EXPONENT = Fields.bigInt("e", "Public Exponent").build(); + static final Set> FIELDS = Collections.concat(AbstractAsymmetricJwk.FIELDS, MODULUS, PUBLIC_EXPONENT); + + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2 + static final List> THUMBPRINT_FIELDS = Collections.>of(PUBLIC_EXPONENT, KTY, MODULUS); + + DefaultRsaPublicJwk(JwkContext ctx) { + super(ctx, THUMBPRINT_FIELDS); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java new file mode 100644 index 00000000..22be55cf --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.SecretJwk; + +import javax.crypto.SecretKey; +import java.util.List; +import java.util.Set; + +class DefaultSecretJwk extends AbstractJwk implements SecretJwk { + + static final String TYPE_VALUE = "oct"; + static final Field K = Fields.bytes("k", "Key Value").setSecret(true).build(); + static final Set> FIELDS = Collections.concat(AbstractJwk.FIELDS, K); + + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2 + static final List> THUMBPRINT_FIELDS = Collections.>of(K, KTY); + + DefaultSecretJwk(JwkContext ctx) { + super(ctx, THUMBPRINT_FIELDS); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java new file mode 100644 index 00000000..63ead591 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SecretKeyBuilder; + +import javax.crypto.SecretKey; +import java.security.Provider; +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DefaultSecretKeyBuilder implements SecretKeyBuilder { + + protected final String JCA_NAME; + protected final int BIT_LENGTH; + protected Provider provider; + protected SecureRandom random; + + public DefaultSecretKeyBuilder(String jcaName, int bitLength) { + this.JCA_NAME = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + if (bitLength % Byte.SIZE != 0) { + String msg = "bitLength must be an even multiple of 8"; + throw new IllegalArgumentException(msg); + } + this.BIT_LENGTH = Assert.gt(bitLength, 0, "bitLength must be > 0"); + setRandom(Randoms.secureRandom()); + } + + @Override + public SecretKeyBuilder setProvider(Provider provider) { + this.provider = provider; + return this; + } + + @Override + public SecretKeyBuilder setRandom(SecureRandom random) { + this.random = random != null ? random : Randoms.secureRandom(); + return this; + } + + @Override + public SecretKey build() { + JcaTemplate template = new JcaTemplate(JCA_NAME, this.provider, this.random); + return template.generateSecretKey(this.BIT_LENGTH); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecureRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecureRequest.java new file mode 100644 index 00000000..776dc8ad --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecureRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SecureRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultSecureRequest extends DefaultRequest implements SecureRequest { + + private final K KEY; + + public DefaultSecureRequest(T payload, Provider provider, SecureRandom secureRandom, K key) { + super(payload, provider, secureRandom); + this.KEY = Assert.notNull(key, "key cannot be null."); + } + + @Override + public K getKey() { + return this.KEY; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifyDigestRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifyDigestRequest.java new file mode 100644 index 00000000..ca41540d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifyDigestRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.VerifyDigestRequest; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultVerifyDigestRequest extends DefaultRequest implements VerifyDigestRequest { + + private final byte[] digest; + + public DefaultVerifyDigestRequest(byte[] payload, Provider provider, SecureRandom secureRandom, byte[] digest) { + super(payload, provider, secureRandom); + this.digest = Assert.notEmpty(digest, "Digest byte array cannot be null or empty."); + } + + @Override + public byte[] getDigest() { + return this.digest; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySecureDigestRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySecureDigestRequest.java new file mode 100644 index 00000000..4684b2a2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySecureDigestRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.VerifySecureDigestRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultVerifySecureDigestRequest extends DefaultSecureRequest implements VerifySecureDigestRequest { + + private final byte[] digest; + + public DefaultVerifySecureDigestRequest(byte[] payload, Provider provider, SecureRandom secureRandom, K key, byte[] digest) { + super(payload, provider, secureRandom, key); + this.digest = Assert.notEmpty(digest, "Digest byte array cannot be null or empty."); + } + + @Override + public byte[] getDigest() { + return this.digest; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultX509Builder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultX509Builder.java new file mode 100644 index 00000000..f3b47545 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultX509Builder.java @@ -0,0 +1,136 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.Functions; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.security.HashAlgorithm; +import io.jsonwebtoken.security.Request; +import io.jsonwebtoken.security.StandardHashAlgorithms; +import io.jsonwebtoken.security.X509Builder; +import io.jsonwebtoken.security.X509Mutator; + +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.List; + +//Consolidates logic between AbstractProtectedHeaderBuilder and AbstractAsymmetricJwkBuilder +public class DefaultX509Builder> implements X509Builder { + + private final X509Mutator mutator; + + private final B builder; + + protected boolean computeX509Sha1Thumbprint; + + /** + * Boolean object indicates 3 states: 1) not configured 2) configured as true, 3) configured as false + */ + protected Boolean computeX509Sha256Thumbprint = null; + + protected List chain; + + protected byte[] sha256Thumbprint; + + private static Function createGetBytesFunction(Class clazz) { + return Functions.wrapFmt(new CheckedFunction() { + @Override + public byte[] apply(X509Certificate cert) throws Exception { + return cert.getEncoded(); + } + }, clazz, "Unable to access X509Certificate encoded bytes necessary to compute thumbprint. Certificate: %s"); + } + + private final Function GET_X509_BYTES; + + public DefaultX509Builder(X509Mutator mutator, B builder, Class getBytesFailedException) { + this.mutator = Assert.notNull(mutator, "X509Mutator cannot be null."); + this.builder = Assert.notNull(builder, "X509Builder cannot be null."); + this.GET_X509_BYTES = createGetBytesFunction(getBytesFailedException); + } + + @Override + public B withX509Sha1Thumbprint(boolean enable) { + this.computeX509Sha1Thumbprint = enable; + return builder; + } + + @Override + public B withX509Sha256Thumbprint(boolean enable) { + this.computeX509Sha256Thumbprint = enable; + return builder; + } + + @Override + public B setX509Url(URI uri) { + this.mutator.setX509Url(uri); + return builder; + } + + @Override + public B setX509CertificateChain(List chain) { + this.mutator.setX509CertificateChain(chain); + this.chain = chain; + return builder; + } + + @Override + public B setX509CertificateSha1Thumbprint(byte[] thumbprint) { + this.mutator.setX509CertificateSha1Thumbprint(thumbprint); + return builder; + } + + @Override + public B setX509CertificateSha256Thumbprint(byte[] thumbprint) { + this.mutator.setX509CertificateSha256Thumbprint(thumbprint); + this.sha256Thumbprint = thumbprint; + return this.builder; + } + + private byte[] computeThumbprint(final X509Certificate cert, HashAlgorithm alg) { + byte[] encoded = GET_X509_BYTES.apply(cert); + Request request = new DefaultRequest<>(encoded, null, null); + return alg.digest(request); + } + + public void apply() { + X509Certificate firstCert = null; + if (!Collections.isEmpty(this.chain)) { + firstCert = this.chain.get(0); + } + + if (computeX509Sha256Thumbprint == null) { //if not specified, enable by default if possible: + computeX509Sha256Thumbprint = firstCert != null && + Objects.isEmpty(this.sha256Thumbprint) // no need to compute if already set + && !computeX509Sha1Thumbprint; // no need if at least one thumbprint will be set + } + + if (firstCert != null) { + if (computeX509Sha1Thumbprint) { + byte[] thumbprint = computeThumbprint(firstCert, DefaultHashAlgorithm.SHA1); + setX509CertificateSha1Thumbprint(thumbprint); + } + if (computeX509Sha256Thumbprint) { + byte[] thumbprint = computeThumbprint(firstCert, StandardHashAlgorithms.get().SHA256); + setX509CertificateSha256Thumbprint(thumbprint); + } + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DelegatingRegistry.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DelegatingRegistry.java new file mode 100644 index 00000000..e6969409 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DelegatingRegistry.java @@ -0,0 +1,46 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Registry; + +import java.util.Collection; + +abstract class DelegatingRegistry implements Registry { + + private final Registry REGISTRY; + + protected DelegatingRegistry(Registry registry) { + this.REGISTRY = Assert.notNull(registry, "Registry cannot be null."); + Assert.notEmpty(this.REGISTRY.values(), "Registry cannot be empty."); + } + + @Override + public Collection values() { + return REGISTRY.values(); + } + + @Override + public T get(String id) throws IllegalArgumentException { + return REGISTRY.get(id); + } + + @Override + public T find(String id) { + return REGISTRY.find(id); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java new file mode 100644 index 00000000..94d67e57 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DirectKeyAlgorithm.java @@ -0,0 +1,51 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.SecretKey; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class DirectKeyAlgorithm implements KeyAlgorithm { + + static final String ID = "dir"; + + @Override + public String getId() { + return ID; + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + SecretKey key = Assert.notNull(request.getPayload(), "Encryption key cannot be null."); + return new DefaultKeyResult(key); + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + Assert.notNull(request, "request cannot be null."); + return Assert.notNull(request.getKey(), "Decryption key cannot be null."); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java new file mode 100644 index 00000000..90b8d56b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DispatchingJwkFactory.java @@ -0,0 +1,112 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class DispatchingJwkFactory implements JwkFactory> { + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Collection> createDefaultFactories() { + List families = new ArrayList<>(3); + families.add(new SecretJwkFactory()); + families.add(new AsymmetricJwkFactory(EcPublicJwkFactory.INSTANCE, new EcPrivateJwkFactory())); + families.add(new AsymmetricJwkFactory(RsaPublicJwkFactory.INSTANCE, new RsaPrivateJwkFactory())); + families.add(new AsymmetricJwkFactory(OctetPublicJwkFactory.INSTANCE, new OctetPrivateJwkFactory())); + return families; + } + + private static final Collection> DEFAULT_FACTORIES = createDefaultFactories(); + static final JwkFactory> DEFAULT_INSTANCE = new DispatchingJwkFactory(); + + private final Collection> factories; + + DispatchingJwkFactory() { + this(DEFAULT_FACTORIES); + } + + @SuppressWarnings("unchecked") + DispatchingJwkFactory(Collection> factories) { + Assert.notEmpty(factories, "FamilyJwkFactory collection cannot be null or empty."); + this.factories = new ArrayList<>(factories.size()); + for (FamilyJwkFactory factory : factories) { + Assert.hasText(factory.getId(), "FamilyJwkFactory.getFactoryId() cannot return null or empty."); + this.factories.add((FamilyJwkFactory) factory); + } + } + + @Override + public JwkContext newContext(JwkContext src, Key key) { + Assert.notNull(src, "JwkContext cannot be null."); + String kty = src.getType(); + assertKeyOrKeyType(key, kty); + for (FamilyJwkFactory factory : this.factories) { + if (factory.supports(key) || factory.supports(src)) { + JwkContext ctx = factory.newContext(src, key); + return Assert.notNull(ctx, "FamilyJwkFactory implementation cannot return null JwkContexts."); + } + } + throw noFamily(key, kty); + } + + private static void assertKeyOrKeyType(Key key, String kty) { + if (key == null && !Strings.hasText(kty)) { + String msg = "Either a Key instance or a " + AbstractJwk.KTY + " value is required to create a JWK."; + throw new InvalidKeyException(msg); + } + } + + @Override + public Jwk createJwk(JwkContext ctx) { + + Assert.notNull(ctx, "JwkContext cannot be null."); + + final Key key = ctx.getKey(); + final String kty = Strings.clean(ctx.getType()); + assertKeyOrKeyType(key, kty); + + for (FamilyJwkFactory factory : this.factories) { + if (factory.supports(ctx)) { + String algFamilyId = Assert.hasText(factory.getId(), "factory id cannot be null or empty."); + if (kty == null) { + ctx.setType(algFamilyId); //ensure the kty is available for the rest of the creation process + } + return factory.createJwk(ctx); + } + } + + // if nothing has been returned at this point, no factory supported the JwkContext, so that's an error: + throw noFamily(key, kty); + } + + private static UnsupportedKeyException noFamily(Key key, String kty) { + String reason = key != null ? + "key of type " + key.getClass().getName() : + "kty value '" + kty + "'"; + String msg = "Unable to create JWK for unrecognized " + reason + + ": there is " + "no known JWK Factory capable of creating JWKs for this key type."; + return new UnsupportedKeyException(msg); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java new file mode 100644 index 00000000..9b580107 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ECCurve.java @@ -0,0 +1,67 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.security.KeyPairBuilder; + +import java.security.AlgorithmParameters; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; + +public class ECCurve extends DefaultCurve { + + static final String KEY_PAIR_GENERATOR_JCA_NAME = "EC"; + + private final ECParameterSpec spec; + + public ECCurve(String id, String jcaName) { + super(id, jcaName); + JcaTemplate template = new JcaTemplate(KEY_PAIR_GENERATOR_JCA_NAME, null); + this.spec = template.withAlgorithmParameters(new CheckedFunction() { + @Override + public ECParameterSpec apply(AlgorithmParameters params) throws Exception { + params.init(new ECGenParameterSpec(getJcaName())); + return params.getParameterSpec(ECParameterSpec.class); + } + }); + } + + public ECParameterSpec toParameterSpec() { + return this.spec; + } + + /** + * Returns {@code true} if this elliptic curve contains the specified {@code point}, {@code false} + * otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) + * Weierstrass form: + *

    + * y2 = x3 + ax + b + *

    + * + * @param point a point that may or may not be defined on this elliptic curve + * @return {@code true} if this elliptic curve contains the specified {@code point}, {@code false} otherwise. + */ + public boolean contains(ECPoint point) { + return AbstractEcJwkFactory.contains(spec.getCurve(), point); + } + + @Override + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder(KEY_PAIR_GENERATOR_JCA_NAME, toParameterSpec()); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java new file mode 100644 index 00000000..1c447b5c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -0,0 +1,110 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; + +class EcPrivateJwkFactory extends AbstractEcJwkFactory { + + private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() + " instance."; + + private static final EcPublicJwkFactory PUB_FACTORY = EcPublicJwkFactory.INSTANCE; + + EcPrivateJwkFactory() { + super(ECPrivateKey.class, DefaultEcPrivateJwk.FIELDS); + } + + @Override + protected boolean supportsKeyValues(JwkContext ctx) { + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultEcPrivateJwk.D.getId()); + } + + @Override + protected EcPrivateJwk createJwkFromKey(JwkContext ctx) { + + ECPrivateKey key = ctx.getKey(); + ECPublicKey ecPublicKey; + + PublicKey publicKey = ctx.getPublicKey(); + if (publicKey != null) { + ecPublicKey = Assert.isInstanceOf(ECPublicKey.class, publicKey, ECPUBKEY_ERR_MSG); + } else { + ecPublicKey = derivePublic(ctx); + } + + // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) + // requires public values to be present in private JWKs, so add them: + + // If a JWK fingerprint has been requested to be the JWK id, ensure we copy over the one computed for the + // public key per https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + boolean copyId = !Strings.hasText(ctx.getId()) && ctx.getIdThumbprintAlgorithm() != null; + + JwkContext pubCtx = PUB_FACTORY.newContext(ctx, ecPublicKey); + EcPublicJwk pubJwk = PUB_FACTORY.createJwk(pubCtx); + ctx.putAll(pubJwk); // add public values to private key context + if (copyId) { + ctx.setId(pubJwk.getId()); + } + + int fieldSize = key.getParams().getCurve().getField().getFieldSize(); + String d = toOctetString(fieldSize, key.getS()); + ctx.put(DefaultEcPrivateJwk.D.getId(), d); + + return new DefaultEcPrivateJwk(ctx, pubJwk); + } + + @Override + protected EcPrivateJwk createJwkFromValues(final JwkContext ctx) { + + FieldReadable reader = new RequiredFieldReader(ctx); + String curveId = reader.get(DefaultEcPublicJwk.CRV); + BigInteger d = reader.get(DefaultEcPrivateJwk.D); + + // We don't actually need the public x,y point coordinates for JVM lookup, but the + // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2) + // requires them to be present and valid for the private key as well, so we assert that here: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultEcPublicJwk.FIELDS, ctx); + EcPublicJwk pubJwk = EcPublicJwkFactory.INSTANCE.createJwk(pubCtx); + + ECParameterSpec spec = getCurveByJwaId(curveId); + final ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(d, spec); + + ECPrivateKey key = generateKey(ctx, new CheckedFunction() { + @Override + public ECPrivateKey apply(KeyFactory kf) throws Exception { + return (ECPrivateKey) kf.generatePrivate(privateSpec); + } + }); + + ctx.setKey(key); + + return new DefaultEcPrivateJwk(ctx, pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java new file mode 100644 index 00000000..dd7c6498 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -0,0 +1,113 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.InvalidKeyException; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.util.Map; + +class EcPublicJwkFactory extends AbstractEcJwkFactory { + + static final EcPublicJwkFactory INSTANCE = new EcPublicJwkFactory(); + + EcPublicJwkFactory() { + super(ECPublicKey.class, DefaultEcPublicJwk.FIELDS); + } + + protected static String keyContainsErrorMessage(String curveId) { + Assert.hasText(curveId, "curveId cannot be null or empty."); + String fmt = "ECPublicKey's ECPoint does not exist on elliptic curve '%s' " + + "and may not be used to create '%s' JWKs."; + return String.format(fmt, curveId, curveId); + } + + protected static String jwkContainsErrorMessage(String curveId, Map jwk) { + Assert.hasText(curveId, "curveId cannot be null or empty."); + String fmt = "EC JWK x,y coordinates do not exist on elliptic curve '%s'. This " + + "could be due simply to an incorrectly-created JWK or possibly an attempted Invalid Curve Attack " + + "(see https://safecurves.cr.yp.to/twist.html for more information)."; + return String.format(fmt, curveId, jwk); + } + + @Override + protected EcPublicJwk createJwkFromKey(JwkContext ctx) { + + ECPublicKey key = ctx.getKey(); + + ECParameterSpec spec = key.getParams(); + EllipticCurve curve = spec.getCurve(); + ECPoint point = key.getW(); + + String curveId = getJwaIdByCurve(curve); + if (!contains(curve, point)) { + String msg = keyContainsErrorMessage(curveId); + throw new InvalidKeyException(msg); + } + + ctx.put(DefaultEcPublicJwk.CRV.getId(), curveId); + + int fieldSize = curve.getField().getFieldSize(); + String x = toOctetString(fieldSize, point.getAffineX()); + ctx.put(DefaultEcPublicJwk.X.getId(), x); + + String y = toOctetString(fieldSize, point.getAffineY()); + ctx.put(DefaultEcPublicJwk.Y.getId(), y); + + return new DefaultEcPublicJwk(ctx); + } + + @Override + protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { + + FieldReadable reader = new RequiredFieldReader(ctx); + String curveId = reader.get(DefaultEcPublicJwk.CRV); + BigInteger x = reader.get(DefaultEcPublicJwk.X); + BigInteger y = reader.get(DefaultEcPublicJwk.Y); + + ECParameterSpec spec = getCurveByJwaId(curveId); + ECPoint point = new ECPoint(x, y); + + if (!contains(spec.getCurve(), point)) { + String msg = jwkContainsErrorMessage(curveId, ctx); + throw new InvalidKeyException(msg); + } + + final ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, spec); + + ECPublicKey key = generateKey(ctx, new CheckedFunction() { + @Override + public ECPublicKey apply(KeyFactory kf) throws Exception { + return (ECPublicKey) kf.generatePublic(pubSpec); + } + }); + + ctx.setKey(key); + + return new DefaultEcPublicJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java new file mode 100644 index 00000000..ccf137c2 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java @@ -0,0 +1,353 @@ +/* + * 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.security; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyPairBuilder; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.VerifySecureDigestRequest; + +import java.math.BigInteger; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.ECKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; + +// @since JJWT_RELEASE_VERSION +public class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { + + private static final String REQD_ORDER_BIT_LENGTH_MSG = "orderBitLength must equal 256, 384, or 521."; + + private static final String DER_ENCODING_SYS_PROPERTY_NAME = + "io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator.derEncodingSupported"; + + private final ECGenParameterSpec KEY_PAIR_GEN_PARAMS; + + private final int orderBitLength; + + /** + * JWA EC (concat formatted) length in bytes for this instance's {@link #orderBitLength}. + */ + private final int signatureByteLength; + private final int sigFieldByteLength; + + private static int shaSize(int orderBitLength) { + return orderBitLength == 521 ? 512 : orderBitLength; + } + + /** + * Returns the correct byte length of an R or S field in a concat signature for the given EC Key order bit length. + * + * + * + * @param orderBitLength the EC Key order bit length (ecKey.getParams().getOrder().bitLength()) + * @return the correct byte length of an R or S field in a concat signature for the given EC Key order bit length. + */ + private static int fieldByteLength(int orderBitLength) { + return (orderBitLength + 7) / Byte.SIZE; + } + + /** + * Returns {@code true} for Order bit lengths defined in the JWA specification, {@code false} otherwise. + * Specifically, returns {@code true} only for values of {@code 256}, {@code 384} and {@code 521}. See + * RFC 7518, Section 3.4 for more. + * + * @param orderBitLength the EC key Order bit length to check + * @return {@code true} for Order bit lengths defined in the JWA specification, {@code false} otherwise. + */ + private static boolean isSupportedOrderBitLength(int orderBitLength) { + // This implementation supports only those defined in the JWA specification. + return orderBitLength == 256 || orderBitLength == 384 || orderBitLength == 521; + } + + public EcSignatureAlgorithm(int orderBitLength) { + super("ES" + shaSize(orderBitLength), "SHA" + shaSize(orderBitLength) + "withECDSA"); + Assert.isTrue(isSupportedOrderBitLength(orderBitLength), REQD_ORDER_BIT_LENGTH_MSG); + String curveName = "secp" + orderBitLength + "r1"; + this.KEY_PAIR_GEN_PARAMS = new ECGenParameterSpec(curveName); + this.orderBitLength = orderBitLength; + this.sigFieldByteLength = fieldByteLength(this.orderBitLength); + this.signatureByteLength = this.sigFieldByteLength * 2; // R bytes + S bytes = concat signature bytes + } + + @Override + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder(ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, this.KEY_PAIR_GEN_PARAMS) + .setProvider(getProvider()) + .setRandom(Randoms.secureRandom()); + } + + @Override + protected void validateKey(Key key, boolean signing) { + super.validateKey(key, signing); + // Some PKCS11 providers and HSMs won't expose the ECKey interface, so we have to check to see if we can cast + // If so, we can provide the additional safety checks: + if (key instanceof ECKey) { + final String name = getId(); + ECKey ecKey = (ECKey) key; + BigInteger order = ecKey.getParams().getOrder(); + int orderBitLength = order.bitLength(); + int sigFieldByteLength = fieldByteLength(orderBitLength); + int concatByteLength = sigFieldByteLength * 2; + + if (concatByteLength != this.signatureByteLength) { + String msg = "The provided Elliptic Curve " + keyType(signing) + " key's size (aka Order bit length) is " + + Bytes.bitsMsg(orderBitLength) + ", but the '" + name + "' algorithm requires EC Keys with " + + Bytes.bitsMsg(this.orderBitLength) + " per " + + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; + throw new InvalidKeyException(msg); + } + } + } + + @Override + protected byte[] doDigest(final SecureRequest request) { + return jca(request).withSignature(new CheckedFunction() { + @Override + public byte[] apply(Signature sig) throws Exception { + sig.initSign(request.getKey()); + sig.update(request.getPayload()); + byte[] signature = sig.sign(); + return transcodeDERToConcat(signature, signatureByteLength); + } + }); + } + + protected boolean isValidRAndS(PublicKey key, byte[] concatSignature) { + if (key instanceof ECKey) { //Some PKCS11 providers and HSMs won't expose the ECKey interface, so we have to check first + ECKey ecKey = (ECKey) key; + BigInteger order = ecKey.getParams().getOrder(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, sigFieldByteLength)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, sigFieldByteLength, concatSignature.length)); + return r.signum() >= 1 && s.signum() >= 1 && r.compareTo(order) < 0 && s.compareTo(order) < 0; + } + return true; + } + + @Override + protected boolean doVerify(final VerifySecureDigestRequest request) { + + final PublicKey key = request.getKey(); + + return jca(request).withSignature(new CheckedFunction() { + @Override + public Boolean apply(Signature sig) { + byte[] concatSignature = request.getDigest(); + byte[] derSignature; + try { + // mandated per https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4 : + if (signatureByteLength != concatSignature.length) { + /* + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature IFF the application + * is configured to do so. This fallback is for backwards compatibility ONLY (to support tokens + * generated by early versions of jjwt) and backwards compatibility will be removed in a future + * version of this library. This fallback is only enabled if the system property is set to 'true' due to + * the risk of CVE-2022-21449 attacks on early JVM versions 15, 17 and 18. + */ + // TODO: remove for 1.0 (DER-encoding support is not in the JWT RFCs) + if (concatSignature[0] == 0x30 && "true".equalsIgnoreCase(System.getProperty(DER_ENCODING_SYS_PROPERTY_NAME))) { + derSignature = concatSignature; + } else { + String msg = "Provided signature is " + Bytes.bytesMsg(concatSignature.length) + " but " + + getId() + " signatures must be exactly " + Bytes.bytesMsg(signatureByteLength) + " per " + + "[RFC 7518, Section 3.4 (validation)](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; + throw new SignatureException(msg); + } + } else { + //guard for JVM security bug CVE-2022-21449: + if (!isValidRAndS(key, concatSignature)) { + return false; + } + + // Convert from concat to DER encoding since + // 1) SHAXXXWithECDSAInP1363Format algorithms are only available on >= JDK 9 and + // 2) the SignatureAlgorithm enum JCA alg names are all SHAXXXwithECDSA (which expects DER formatting) + derSignature = transcodeConcatToDER(concatSignature); + } + + sig.initVerify(key); + sig.update(request.getPayload()); + return sig.verify(derSignature); + + } catch (Exception e) { + String msg = "Unable to verify Elliptic Curve signature using provided ECPublicKey: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + }); + } + + /** + * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated + * R + S format expected by ECDSA JWS. + * + * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. + * @param outputLength The expected length of the ECDSA JWS signature. + * @return The ECDSA JWS encoded signature. + * @throws JwtException If the ASN.1/DER signature format is invalid. + * @author Martin Treurnicht via 61510dfca58dd40b4b32c708935126785dcff48c + */ + public static byte[] transcodeDERToConcat(final byte[] derSignature, int outputLength) throws JwtException { + + if (derSignature.length < 8 || derSignature[0] != 48) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + if (derSignature[1] > 0) { + offset = 2; + } else if (derSignature[1] == (byte) 0x81) { + offset = 3; + } else { + throw new JwtException("Invalid ECDSA signature format"); + } + + byte rLength = derSignature[offset + 1]; + + int i = rLength; + while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { + i--; + } + + byte sLength = derSignature[offset + 2 + rLength + 1]; + + int j = sLength; + while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { + j--; + } + + int rawLen = Math.max(i, j); + rawLen = Math.max(rawLen, outputLength / 2); + + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { + throw new JwtException("Invalid ECDSA signature format"); + } + + final byte[] concatSignature = new byte[2 * rawLen]; + + System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); + System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); + + return concatSignature; + } + + /** + * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by the JCA verifier. + * + * @param jwsSignature The JWS signature, consisting of the concatenated R and S values. Must not be {@code null}. + * @return The ASN.1/DER encoded signature. + * @throws JwtException If the ECDSA JWS signature format is invalid. + */ + public static byte[] transcodeConcatToDER(byte[] jwsSignature) throws JwtException { + try { + return concatToDER(jwsSignature); + } catch (Exception e) { // CVE-2022-21449 guard + String msg = "Invalid ECDSA signature format."; + throw new SignatureException(msg, e); + } + } + + /** + * Converts the specified concat-encoded signature to a DER-encoded signature. + * + * @param jwsSignature concat-encoded signature + * @return correpsonding DER-encoded signature + * @throws ArrayIndexOutOfBoundsException if the signature cannot be converted + * @author Martin Treurnicht via 61510dfca58dd40b4b32c708935126785dcff48c + */ + private static byte[] concatToDER(byte[] jwsSignature) throws ArrayIndexOutOfBoundsException { + + int rawLen = jwsSignature.length / 2; + + int i = rawLen; + + while ((i > 0) && (jwsSignature[rawLen - i] == 0)) { + i--; + } + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k = rawLen; + + while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { + k--; + } + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + + final byte[] derSignature; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java new file mode 100644 index 00000000..a85c1035 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -0,0 +1,287 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.DefaultJweHeader; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyLengthSupplier; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.OctetPublicJwk; +import io.jsonwebtoken.security.ProtoJwkBuilder; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.Request; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +class EcdhKeyAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { + + protected static final String JCA_NAME = "ECDH"; + protected static final String XDH_JCA_NAME = "XDH"; + protected static final String DEFAULT_ID = JCA_NAME + "-ES"; + + // Per https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2, 2nd paragraph: + // Key derivation is performed using the Concat KDF, as defined in + // Section 5.8.1 of [NIST.800-56A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf), + // where the Digest Method is SHA-256. + private static final String CONCAT_KDF_HASH_ALG_NAME = "SHA-256"; + private static final ConcatKDF CONCAT_KDF = new ConcatKDF(CONCAT_KDF_HASH_ALG_NAME); + public static final String KEK_TYPE_MESSAGE = "Key Encryption Key must be a " + ECKey.class.getName() + + " or valid Edwards Curve PublicKey instance."; + public static final String KDK_TYPE_MESSAGE = "Key Decryption Key must be a " + ECKey.class.getName() + + " or valid Edwards Curve PrivateKey instance."; + + private final KeyAlgorithm WRAP_ALG; + + private static String idFor(KeyAlgorithm wrapAlg) { + return wrapAlg instanceof DirectKeyAlgorithm ? DEFAULT_ID : DEFAULT_ID + "+" + wrapAlg.getId(); + } + + EcdhKeyAlgorithm() { + // default ECDH-ES doesn't do a wrap, so we use DirectKeyAlgorithm which is a no-op. That is, we're using + // the Null Object Design Pattern so we don't have to check for null depending on if key wrapping is used or not + this(new DirectKeyAlgorithm()); + } + + EcdhKeyAlgorithm(KeyAlgorithm wrapAlg) { + super(idFor(wrapAlg), JCA_NAME); + this.WRAP_ALG = Assert.notNull(wrapAlg, "Wrap algorithm cannot be null."); + } + + //visible for testing, for non-Edwards elliptic curves + protected KeyPair generateKeyPair(final Request request, final ECParameterSpec spec) { + Assert.notNull(spec, "request key params cannot be null."); + JcaTemplate template = new JcaTemplate(ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, getProvider(request), ensureSecureRandom(request)); + return template.generateKeyPair(spec); + } + + //visible for testing, for Edwards elliptic curves + protected KeyPair generateKeyPair(SecureRandom random, EdwardsCurve curve, Provider provider) { + return curve.keyPairBuilder().setProvider(provider).setRandom(random).build(); + } + + protected byte[] generateZ(final KeyRequest request, final PublicKey pub, final PrivateKey priv) { + return jca(request).withKeyAgreement(new CheckedFunction() { + @Override + public byte[] apply(KeyAgreement keyAgreement) throws Exception { + keyAgreement.init(priv, ensureSecureRandom(request)); + keyAgreement.doPhase(pub, true); + return keyAgreement.generateSecret(); + } + }); + } + + protected String getConcatKDFAlgorithmId(AeadAlgorithm enc) { + return this.WRAP_ALG instanceof DirectKeyAlgorithm ? + Assert.hasText(enc.getId(), "AeadAlgorithm id cannot be null or empty.") : + getId(); + } + + private byte[] createOtherInfo(int keydatalen, String AlgorithmID, byte[] PartyUInfo, byte[] PartyVInfo) { + + // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2 "AlgorithmID": + Assert.hasText(AlgorithmID, "AlgorithmId cannot be null or empty."); + byte[] algIdBytes = AlgorithmID.getBytes(StandardCharsets.US_ASCII); + + PartyUInfo = Arrays.length(PartyUInfo) == 0 ? Bytes.EMPTY : PartyUInfo; // ensure not null + PartyVInfo = Arrays.length(PartyVInfo) == 0 ? Bytes.EMPTY : PartyVInfo; // ensure not null + + // Values and order defined in https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2 and + // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf section 5.8.1.2 : + return Bytes.concat( + Bytes.toBytes(algIdBytes.length), algIdBytes, // AlgorithmID + Bytes.toBytes(PartyUInfo.length), PartyUInfo, // PartyUInfo + Bytes.toBytes(PartyVInfo.length), PartyVInfo, // PartyVInfo + Bytes.toBytes(keydatalen), // SuppPubInfo per https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2 + Bytes.EMPTY // SuppPrivInfo empty per https://www.rfc-editor.org/rfc/rfc7518.html#section-4.6.2 + ); + } + + private int getKeyBitLength(AeadAlgorithm enc) { + int bitLength = this.WRAP_ALG instanceof KeyLengthSupplier ? + ((KeyLengthSupplier) this.WRAP_ALG).getKeyBitLength() : enc.getKeyBitLength(); + return Assert.gt(bitLength, 0, "Algorithm keyBitLength must be > 0"); + } + + private SecretKey deriveKey(KeyRequest request, PublicKey publicKey, PrivateKey privateKey) { + AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + int requiredCekBitLen = getKeyBitLength(enc); + final String AlgorithmID = getConcatKDFAlgorithmId(enc); + byte[] apu = request.getHeader().getAgreementPartyUInfo(); + byte[] apv = request.getHeader().getAgreementPartyVInfo(); + byte[] OtherInfo = createOtherInfo(requiredCekBitLen, AlgorithmID, apu, apv); + byte[] Z = generateZ(request, publicKey, privateKey); + return CONCAT_KDF.deriveKey(Z, requiredCekBitLen, OtherInfo); + } + + @Override + protected String getJcaName(Request request) { + if (request instanceof SecureRequest) { + return ((SecureRequest) request).getKey() instanceof ECKey ? super.getJcaName(request) : XDH_JCA_NAME; + } else { + return request.getPayload() instanceof ECKey ? super.getJcaName(request) : XDH_JCA_NAME; + } + } + + private static EdwardsCurve assertAgreement(Key key, String exMsg) { + EdwardsCurve curve; + try { + curve = EdwardsCurve.forKey(key); + } catch (Exception e) { + throw new UnsupportedKeyException(exMsg + " Cause: " + e.getMessage(), e); + } + Assert.stateNotNull(curve, "EdwardsCurve instance cannot be null."); + if (curve.isSignatureCurve()) { + String msg = curve.getId() + " keys may not be used with ECDH-ES key agreement algorithms per " + + "https://www.rfc-editor.org/rfc/rfc8037#section-3.1"; + throw new UnsupportedKeyException(msg); + } + return curve; + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + Assert.notNull(request, "Request cannot be null."); + JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + PublicKey publicKey = Assert.notNull(request.getPayload(), "Encryption PublicKey cannot be null."); + + KeyPair pair; // generated (ephemeral) key pair + final SecureRandom random = ensureSecureRandom(request); + ProtoJwkBuilder jwkBuilder = Jwks.builder().setRandom(random); + + if (publicKey instanceof ECKey) { + ECKey ecPublicKey = (ECKey) publicKey; + ECParameterSpec spec = Assert.notNull(ecPublicKey.getParams(), "Encryption PublicKey params cannot be null."); + // note: we don't need to validate if specified key's point is on a supported curve here + // because that will automatically be asserted when using Jwks.builder().... below + pair = generateKeyPair(request, spec); + // assert pair key types: + KeyPairs.getKey(pair, ECPublicKey.class); + KeyPairs.getKey(pair, ECPrivateKey.class); + } else { // it must be an edwards curve key + EdwardsCurve curve = assertAgreement(publicKey, KEK_TYPE_MESSAGE); + Provider provider = request.getProvider(); + Provider curveProvider = curve.getProvider(); // only non-null if not natively supported by the JVM + if (provider == null && curveProvider != null) { // ensure that BC can be used if necessary: + provider = curveProvider; + request = new DefaultKeyRequest<>(request.getPayload(), provider, random, + request.getHeader(), request.getEncryptionAlgorithm()); + } + pair = generateKeyPair(random, curve, provider); + jwkBuilder.setProvider(provider); + } + + Assert.stateNotNull(pair, "Internal implementation state: KeyPair cannot be null."); + + // This asserts that the generated public key (and therefore the request key) is on a JWK-supported curve: + PublicJwk jwk = jwkBuilder.forKey(pair.getPublic()).build(); + + final SecretKey derived = deriveKey(request, publicKey, pair.getPrivate()); + + DefaultKeyRequest wrapReq = + new DefaultKeyRequest<>(derived, request.getProvider(), request.getSecureRandom(), + request.getHeader(), request.getEncryptionAlgorithm()); + KeyResult result = WRAP_ALG.getEncryptionKey(wrapReq); + + header.put(DefaultJweHeader.EPK.getId(), jwk); + + return result; + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + + Assert.notNull(request, "Request cannot be null."); + JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + PrivateKey privateKey = Assert.notNull(request.getKey(), "Decryption PrivateKey cannot be null."); + FieldReadable reader = new RequiredFieldReader(header); + PublicJwk epk = reader.get(DefaultJweHeader.EPK); + + if (privateKey instanceof ECKey) { + ECKey ecPrivateKey = (ECKey) privateKey; + if (!(epk instanceof EcPublicJwk)) { + String msg = "JWE Header " + DefaultJweHeader.EPK + " value is not a supported Elliptic Curve " + + "Public JWK. Value: " + epk; + throw new UnsupportedKeyException(msg); + } + EcPublicJwk ecEpk = (EcPublicJwk) epk; + // While the EPK might be on a JWA-supported NIST curve, it must be on the private key's exact curve: + if (!EcPublicJwkFactory.contains(ecPrivateKey.getParams().getCurve(), ecEpk.toKey().getW())) { + String msg = "JWE Header " + DefaultJweHeader.EPK + " value does not represent " + + "a point on the expected curve."; + throw new InvalidKeyException(msg); + } + } else { // it must be an Edwards Curve key + EdwardsCurve privateKeyCurve = assertAgreement(privateKey, KDK_TYPE_MESSAGE); + if (!(epk instanceof OctetPublicJwk)) { + String msg = "JWE Header " + DefaultJweHeader.EPK + " value is not a supported Elliptic Curve " + + "Public JWK. Value: " + epk; + throw new UnsupportedKeyException(msg); + } + OctetPublicJwk oEpk = (OctetPublicJwk) epk; + EdwardsCurve epkCurve = EdwardsCurve.forKey(oEpk.toKey()); + if (!privateKeyCurve.equals(epkCurve)) { + String msg = "JWE Header " + DefaultJweHeader.EPK + " value does not represent a point " + + "on the expected curve. Value: " + oEpk; + throw new InvalidKeyException(msg); + } + Provider curveProvider = privateKeyCurve.getProvider(); + if (request.getProvider() == null && curveProvider != null) { // ensure that BC can be used if necessary: + request = new DefaultDecryptionKeyRequest<>(request.getPayload(), curveProvider, + ensureSecureRandom(request), request.getHeader(), request.getEncryptionAlgorithm(), + request.getKey()); + } + } + + final SecretKey derived = deriveKey(request, epk.toKey(), privateKey); + + DecryptionKeyRequest unwrapReq = + new DefaultDecryptionKeyRequest<>(request.getPayload(), request.getProvider(), + request.getSecureRandom(), header, request.getEncryptionAlgorithm(), derived); + + return WRAP_ALG.getDecryptionKey(unwrapReq); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java new file mode 100644 index 00000000..590b84d7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java @@ -0,0 +1,81 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyPairBuilder; +import io.jsonwebtoken.security.Request; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.UnsupportedKeyException; +import io.jsonwebtoken.security.VerifyDigestRequest; + +import java.security.Key; + +public class EdSignatureAlgorithm extends AbstractSignatureAlgorithm { + + private static final String ID = "EdDSA"; + + private final EdwardsCurve preferredCurve; + + public EdSignatureAlgorithm() { + super(ID, ID); + this.preferredCurve = EdwardsCurve.Ed448; + // EdDSA is not available natively until JDK 15, so try to load BC as a backup provider if possible: + setProvider(this.preferredCurve.getProvider()); + } + + public EdSignatureAlgorithm(EdwardsCurve preferredCurve) { + super(ID, preferredCurve.getJcaName()); + this.preferredCurve = Assert.notNull(preferredCurve, "preferredCurve cannot be null."); + Assert.isTrue(preferredCurve.isSignatureCurve(), "EdwardsCurve must be a signature curve, not a key agreement curve."); + setProvider(preferredCurve.getProvider()); + } + + @Override + protected String getJcaName(Request request) { + SecureRequest req = Assert.isInstanceOf(SecureRequest.class, request, "SecureRequests are required."); + Key key = req.getKey(); + + // If we're signing, and this instance's algorithm name is the default/generic 'EdDSA', then prefer the + // signing key's curve algorithm ID. This ensures the most specific JCA algorithm is used for signing, + // (while generic 'EdDSA' is fine for validation) + String jcaName = getJcaName(); //default for JCA interaction + boolean signing = !(request instanceof VerifyDigestRequest); + if (ID.equals(jcaName) && signing) { // see if we can get a more-specific curve algorithm identifier: + EdwardsCurve curve = EdwardsCurve.findByKey(key); + if (curve != null) { + jcaName = curve.getJcaName(); // prefer the key's specific curve algorithm identifier during signing + } + } + return jcaName; + } + + @Override + public KeyPairBuilder keyPairBuilder() { + return this.preferredCurve.keyPairBuilder(); + } + + @Override + protected void validateKey(Key key, boolean signing) { + super.validateKey(key, signing); + EdwardsCurve curve = EdwardsCurve.findByKey(key); + if (curve != null && !curve.isSignatureCurve()) { + String msg = curve.getId() + " keys may not be used with " + getId() + " digital signatures per " + + "https://www.rfc-editor.org/rfc/rfc8037#section-3.2"; + throw new UnsupportedKeyException(msg); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EdwardsCurve.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EdwardsCurve.java new file mode 100644 index 00000000..e1c4ccd9 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EdwardsCurve.java @@ -0,0 +1,476 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.Functions; +import io.jsonwebtoken.impl.lang.OptionalCtorInvoker; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyException; +import io.jsonwebtoken.security.KeyLengthSupplier; +import io.jsonwebtoken.security.KeyPairBuilder; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +public class EdwardsCurve extends DefaultCurve implements KeyLengthSupplier { + + private static final String OID_PREFIX = "1.3.101."; + + // DER-encoded edwards keys have this exact sequence identifying the type of key that follows. The trailing + // byte is the exact edwards curve subsection OID terminal node id. + private static final byte[] DER_OID_PREFIX = new byte[]{0x06, 0x03, 0x2B, 0x65}; + + private static final String NAMED_PARAM_SPEC_FQCN = "java.security.spec.NamedParameterSpec"; // JDK >= 11 + private static final String XEC_PRIV_KEY_SPEC_FQCN = "java.security.spec.XECPrivateKeySpec"; // JDK >= 11 + private static final String EDEC_PRIV_KEY_SPEC_FQCN = "java.security.spec.EdECPrivateKeySpec"; // JDK >= 15 + + private static final Function CURVE_NAME_FINDER = new NamedParameterSpecValueFinder(); + private static final OptionalCtorInvoker NAMED_PARAM_SPEC_CTOR = + new OptionalCtorInvoker<>(NAMED_PARAM_SPEC_FQCN, String.class); + static final OptionalCtorInvoker XEC_PRIV_KEY_SPEC_CTOR = + new OptionalCtorInvoker<>(XEC_PRIV_KEY_SPEC_FQCN, AlgorithmParameterSpec.class, byte[].class); + static final OptionalCtorInvoker EDEC_PRIV_KEY_SPEC_CTOR = + new OptionalCtorInvoker<>(EDEC_PRIV_KEY_SPEC_FQCN, NAMED_PARAM_SPEC_FQCN, byte[].class); + + public static final EdwardsCurve X25519 = new EdwardsCurve("X25519", 110); // Requires JDK >= 11 or BC + public static final EdwardsCurve X448 = new EdwardsCurve("X448", 111); // Requires JDK >= 11 or BC + public static final EdwardsCurve Ed25519 = new EdwardsCurve("Ed25519", 112); // Requires JDK >= 15 or BC + public static final EdwardsCurve Ed448 = new EdwardsCurve("Ed448", 113); // Requires JDK >= 15 or BC + + public static final Collection VALUES = Collections.of(X25519, X448, Ed25519, Ed448); + + private static final Map REGISTRY; + + private static final Map BY_OID_TERMINAL_NODE; + + static { + REGISTRY = new LinkedHashMap<>(8); + BY_OID_TERMINAL_NODE = new LinkedHashMap<>(4); + for (EdwardsCurve curve : VALUES) { + int subcategoryId = curve.DER_OID[curve.DER_OID.length - 1]; + BY_OID_TERMINAL_NODE.put(subcategoryId, curve); + REGISTRY.put(curve.getId(), curve); + REGISTRY.put(curve.OID, curve); // add OID as an alias for alg/id lookups + } + } + + private final String OID; + + /** + * The byte sequence within an DER-encoded key that indicates an Edwards curve encoded key follows. DER (hex) + * notation: + *
    +     * 06 03       ;   OBJECT IDENTIFIER (3 bytes long)
    +     * |  2B 65 $I ;     "1.3.101.$I" for Edwards alg OID, where $I = 6E, 6F, 70, or 71 (decimal 110, 111, 112, or 113)
    +     * 
    + */ + final byte[] DER_OID; + + private final int keyBitLength; + + private final int KEY_PAIR_GENERATOR_BIT_LENGTH; + + private final int encodedKeyByteLength; + + /** + * X.509 (DER) encoding of a public key associated with this curve as a prefix (that is, without the + * actual encoded key material at the end). Appending the public key material directly to the end of this value + * results in a complete X.509 (DER) encoded public key. DER (hex) notation: + *
    +     * 30 $M               ; DER SEQUENCE ($M bytes long), where $M = encodedKeyByteLength + 10
    +     *    30 05            ;   DER SEQUENCE (5 bytes long)
    +     *       06 03         ;     OBJECT IDENTIFIER (3 bytes long)
    +     *          2B 65 $I   ;       "1.3.101.$I" for Edwards alg OID, where $I = 6E, 6F, 70, or 71 (110, 111, 112, or 113 decimal)
    +     *    03 $S            ;   DER BIT STRING ($S bytes long), where $S = encodedKeyByteLength + 1
    +     *       00            ;     DER bit string marker indicating zero unused bits at the end of the bit string
    +     *       XX XX XX ...  ;     encoded key material (not included in this PREFIX byte array variable)
    +     * 
    + */ + private final byte[] PUBLIC_KEY_DER_PREFIX; + + /** + * PKCS8 (DER) Version 1 encoding of a private key associated with this curve, as a prefix (that is, + * without actual encoded key material at the end). Appending the private key material directly to the + * end of this value results in a complete PKCS8 (DER) V1 encoded private key. DER (hex) notation: + *
    +     * 30 $M                  ; DER SEQUENCE ($M bytes long), where $M = encodedKeyByteLength + 14
    +     *    02 01               ;   DER INTEGER (1 byte long)
    +     *       00               ;     zero (private key encoding version V1)
    +     *    30 05               ;   DER SEQUENCE (5 bytes long)
    +     *       06 03            ;     OBJECT IDENTIFIER (3 bytes long). This is the edwards algorithm ID.
    +     *          2B 65 $I      ;       "1.3.101.$I" for Edwards alg OID, where $I = 6E, 6F, 70, or 71 (110, 111, 112, or 113 decimal)
    +     *    04 $B               ;   DER SEQUENCE ($B bytes long, where $B = encodedKeyByteLength + 2
    +     *       04 $K            ;     DER SEQUENCE ($K bytes long), where $K = encodedKeyByteLength
    +     *          XX XX XX ...  ;       encoded key material (not included in this PREFIX byte array variable)
    +     * 
    + */ + private final byte[] PRIVATE_KEY_DER_PREFIX; + + private final AlgorithmParameterSpec NAMED_PARAMETER_SPEC; // null on <= JDK 10 + + private final Function PRIVATE_KEY_SPEC_FACTORY; + + /** + * {@code true} IFF the curve is used for digital signatures, {@code false} if used for key agreement + */ + private final boolean signatureCurve; + + EdwardsCurve(final String id, int oidTerminalNode) { + super(id, id, // JWT ID and JCA name happen to be identical + // fall back to BouncyCastle if < JDK 11 (for XDH curves) or < JDK 15 (for EdDSA curves) if necessary: + Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + public KeyPairGenerator get() throws Exception { + return KeyPairGenerator.getInstance(id); + } + }))); + + // OIDs (with terminal node IDs) defined here: https://www.rfc-editor.org/rfc/rfc8410#section-3 + // X25519 (oid 1.3.101.110) and X448 (oid 1.3.101.111) have 256 bits + // Ed25519 (oid 1.3.101.112) has 256 bits + // Ed448 (oid 1.3.101.113) has 456 (448 + 8) bits + // See https://www.rfc-editor.org/rfc/rfc8032 + switch (oidTerminalNode) { + case 110: + case 112: + this.keyBitLength = 256; + break; + case 111: + this.keyBitLength = 448; + break; + case 113: + this.keyBitLength = 448 + Byte.SIZE; + break; + default: + String msg = "Invalid Edwards Curve ASN.1 OID terminal node value"; + throw new IllegalArgumentException(msg); + } + + this.OID = OID_PREFIX + oidTerminalNode; + this.signatureCurve = (oidTerminalNode == 112 || oidTerminalNode == 113); + byte[] suffix = new byte[]{(byte) oidTerminalNode}; + this.DER_OID = Bytes.concat(DER_OID_PREFIX, suffix); + this.encodedKeyByteLength = (this.keyBitLength + 7) / 8; + + this.PUBLIC_KEY_DER_PREFIX = Bytes.concat( + new byte[]{ + 0x30, (byte) (this.encodedKeyByteLength + 10), + 0x30, 0x05}, // DER SEQUENCE of 5 bytes to follow (i.e. the OID) + this.DER_OID, + new byte[]{ + 0x03, + (byte) (this.encodedKeyByteLength + 1), + 0x00} + ); + + byte[] keyPrefix = new byte[]{ + 0x04, (byte) (this.encodedKeyByteLength + 2), + 0x04, (byte) this.encodedKeyByteLength}; + + this.PRIVATE_KEY_DER_PREFIX = Bytes.concat( + new byte[]{ + 0x30, + (byte) (this.encodedKeyByteLength + 10 + keyPrefix.length), + 0x02, 0x01, 0x00, // encoding version 1 (integer, 1 byte, value 0) + 0x30, 0x05}, // DER SEQUENCE of 5 bytes to follow (i.e. the OID) + this.DER_OID, + keyPrefix + ); + + this.NAMED_PARAMETER_SPEC = NAMED_PARAM_SPEC_CTOR.apply(id); // null on <= JDK 10 + Function paramKeySpecFn = paramKeySpecFactory(NAMED_PARAMETER_SPEC, signatureCurve); + Function pkcs8KeySpecFn = new Pkcs8KeySpecFactory(this.PRIVATE_KEY_DER_PREFIX); + // prefer the JDK KeySpec classes first, and fall back to PKCS8 encoding if unavailable: + this.PRIVATE_KEY_SPEC_FACTORY = Functions.firstResult(paramKeySpecFn, pkcs8KeySpecFn); + + // The Sun CE KeyPairGenerator implementation that we'll use to derive PublicKeys with is problematic here: + // + // [RFC 7748](https://www.rfc-editor.org/rfc/rfc7748) is clear that X25519 keys are 32 bytes (256 bits) and + // X448 keys are 56 bytes (448 bits); see the test vectors in + // [RFC 7748, Section 5.2](https://www.rfc-editor.org/rfc/rfc7748#section-5.2). + // + // Additionally [RFC 8032, Section 1](https://www.rfc-editor.org/rfc/rfc8032#section-1) is clear that + // Ed25519 keys are 32 bytes (256 bits) and Ed448 keys are 57 bytes (456 bits). + // + // HOWEVER: + // + // The JDK KeyPairGenerator#initialize(keysize, random) method that we use below ONLY accepts + // values of '255' and '448', which clearly are `keysize`s that do not match the RFC mandatory lengths. + // The Sun CE implementation: + // https://github.com/AdoptOpenJDK/openjdk-jdk15/blob/4a588d89f01a650d90432cc14697a5a2ae2c97d3/src/jdk.crypto.ec/share/classes/sun/security/ec/ed/EdDSAParameters.java#L252-L297 + // + // (see the two `int bits = 255` and `int bits = 448` lines). + // + // It is strange that the JDK implementation does not match the RFC-specified key length values. + // As such, we 'normalize' our curve's (RFC-correct) key bit length to values that the Sun CE + // (and also BouncyCastle) will recognize: + this.KEY_PAIR_GENERATOR_BIT_LENGTH = this.keyBitLength >= 448 ? 448 : 255; + } + + // visible for testing + protected static Function paramKeySpecFactory(AlgorithmParameterSpec spec, boolean signatureCurve) { + if (spec == null) { + return Functions.forNull(); + } + return new ParameterizedKeySpecFactory(spec, signatureCurve ? EDEC_PRIV_KEY_SPEC_CTOR : XEC_PRIV_KEY_SPEC_CTOR); + } + + @Override + public int getKeyBitLength() { + return this.keyBitLength; + } + + public byte[] getKeyMaterial(Key key) { + try { + return doGetKeyMaterial(key); // can throw assertion and ArrayIndexOutOfBound exception on invalid input + } catch (Throwable t) { + if (t instanceof KeyException) { //propagate + throw (KeyException) t; + } + String msg = "Invalid " + getId() + " DER encoding: " + t.getMessage(); + throw new InvalidKeyException(msg, t); + } + } + + /** + * Parses the DER-encoding of the specified key + * + * @param key the Edwards curve key + * @return the key value, encoded according to RFC 8032 + * @throws RuntimeException if the key's encoded bytes do not reflect a validly DER-encoded edwards key + */ + protected byte[] doGetKeyMaterial(Key key) { + byte[] encoded = KeysBridge.getEncoded(key); + int i = Bytes.indexOf(encoded, DER_OID); + Assert.gt(i, -1, "Missing or incorrect algorithm OID."); + i = i + DER_OID.length; + int keyLen = 0; + if (encoded[i] == 0x05) { // NULL terminator, next should be zero byte indicator + int unusedBytes = encoded[++i]; + Assert.eq(0, unusedBytes, "OID NULL terminator should indicate zero unused bytes."); + i++; + } + if (encoded[i] == 0x03) { // DER bit stream, Public Key + i++; + keyLen = encoded[i++]; + int unusedBytes = encoded[i++]; + Assert.eq(0, unusedBytes, "BIT STREAM should not indicate unused bytes."); + keyLen--; + } else if (encoded[i] == 0x04) { // DER octet sequence, Private Key. Key length follows as next byte. + i++; + keyLen = encoded[i++]; + if (encoded[i] == 0x04) { // DER octet sequence, key length follows as next byte. + i++; // skip sequence marker + keyLen = encoded[i++]; // next byte is length + } + } + Assert.eq(this.encodedKeyByteLength, keyLen, "Invalid key length."); + byte[] result = Arrays.copyOfRange(encoded, i, i + keyLen); + keyLen = Bytes.length(result); + Assert.eq(this.encodedKeyByteLength, keyLen, "Invalid key length."); + return result; + } + + protected Provider fallback(Provider provider) { + if (provider == null) { + provider = getProvider(); + } + return provider; + } + + private void assertLength(byte[] raw, boolean isPublic) { + int len = Bytes.length(raw); + if (len != this.encodedKeyByteLength) { + String msg = "Invalid " + getId() + " encoded " + (isPublic ? "PublicKey" : "PrivateKey") + + " length. Should be " + Bytes.bytesMsg(this.encodedKeyByteLength) + ", found " + + Bytes.bytesMsg(len) + "."; + throw new InvalidKeyException(msg); + } + } + + public PublicKey toPublicKey(byte[] x, Provider provider) { + assertLength(x, true); + final byte[] encoded = Bytes.concat(this.PUBLIC_KEY_DER_PREFIX, x); + final X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded); + JcaTemplate template = new JcaTemplate(getJcaName(), fallback(provider)); + return template.withKeyFactory(new CheckedFunction() { + @Override + public PublicKey apply(KeyFactory keyFactory) throws Exception { + return keyFactory.generatePublic(spec); + } + }); + } + + public PrivateKey toPrivateKey(byte[] d, Provider provider) { + assertLength(d, false); + final KeySpec spec = this.PRIVATE_KEY_SPEC_FACTORY.apply(d); + JcaTemplate template = new JcaTemplate(getJcaName(), fallback(provider)); + return template.withKeyFactory(new CheckedFunction() { + @Override + public PrivateKey apply(KeyFactory keyFactory) throws Exception { + return keyFactory.generatePrivate(spec); + } + }); + } + + /** + * Returns {@code true} if this curve is used to compute signatures, {@code false} if used for key agreement. + * + * @return {@code true} if this curve is used to compute signatures, {@code false} if used for key agreement. + */ + public boolean isSignatureCurve() { + return this.signatureCurve; + } + + @Override + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder(getJcaName(), KEY_PAIR_GENERATOR_BIT_LENGTH).setProvider(getProvider()); + } + + public static boolean isEdwards(Key key) { + if (key == null) { + return false; + } + String alg = Strings.clean(key.getAlgorithm()); + return "EdDSA".equals(alg) || "XDH".equals(alg) || findByKey(key) != null; + } + + /** + * Computes the PublicKey associated with the specified Edwards-curve PrivateKey. + * + * @param pk the Edwards-curve {@code PrivateKey} to inspect. + * @return the PublicKey associated with the specified Edwards-curve PrivateKey. + * @throws KeyException if the PrivateKey is not an Edwards-curve key or unable to access the PrivateKey's + * material. + */ + public static PublicKey derivePublic(PrivateKey pk) throws KeyException { + return EdwardsPublicKeyDeriver.INSTANCE.apply(pk); + } + + public static EdwardsCurve findById(String id) { + return REGISTRY.get(id); + } + + public static EdwardsCurve findByKey(Key key) { + if (key == null) { + return null; + } + + String alg = key.getAlgorithm(); + EdwardsCurve curve = findById(alg); // try constant time lookup first + if (curve == null) { // Fall back to JDK 11+ NamedParameterSpec access if possible + alg = CURVE_NAME_FINDER.apply(key); + curve = findById(alg); + } + if (curve == null) { // Fall back to key encoding if possible: + // Try to find the Key DER algorithm OID: + byte[] encoded = KeysBridge.findEncoded(key); + if (!Bytes.isEmpty(encoded)) { + int oidTerminalNode = findOidTerminalNode(encoded); + curve = BY_OID_TERMINAL_NODE.get(oidTerminalNode); + } + } + + //TODO: check if key exists on discovered curve via equation + + return curve; + } + + private static int findOidTerminalNode(byte[] encoded) { + int index = Bytes.indexOf(encoded, DER_OID_PREFIX); + if (index > -1) { + index = index + DER_OID_PREFIX.length; + if (index < encoded.length) { + return encoded[index]; + } + } + return -1; + } + + public static EdwardsCurve forKey(Key key) { + Assert.notNull(key, "Key cannot be null."); + EdwardsCurve curve = findByKey(key); + if (curve == null) { + String msg = key.getClass().getName() + " with algorithm '" + key.getAlgorithm() + + "' is not a recognized Edwards Curve key."; + throw new UnsupportedKeyException(msg); + } + //TODO: assert key exists on discovered curve via equation + return curve; + } + + @SuppressWarnings("UnusedReturnValue") + static K assertEdwards(K key) { + forKey(key); // will throw UnsupportedKeyException if the key is not an Edwards key + return key; + } + + private static final class Pkcs8KeySpecFactory implements Function { + private final byte[] PREFIX; + + private Pkcs8KeySpecFactory(byte[] pkcs8EncodedKeyPrefix) { + this.PREFIX = Assert.notEmpty(pkcs8EncodedKeyPrefix, "pkcs8EncodedKeyPrefix cannot be null or empty."); + } + + @Override + public KeySpec apply(byte[] d) { + Assert.notEmpty(d, "Key bytes cannot be null or empty."); + byte[] encoded = Bytes.concat(PREFIX, d); + return new PKCS8EncodedKeySpec(encoded); + } + } + + // visible for testing + protected static final class ParameterizedKeySpecFactory implements Function { + + private final AlgorithmParameterSpec params; + + private final Function keySpecFactory; + + ParameterizedKeySpecFactory(AlgorithmParameterSpec params, Function keySpecFactory) { + this.params = Assert.notNull(params, "AlgorithmParameterSpec cannot be null."); + this.keySpecFactory = Assert.notNull(keySpecFactory, "KeySpec factory function cannot be null."); + } + + @Override + public KeySpec apply(byte[] d) { + Assert.notEmpty(d, "Key bytes cannot be null or empty."); + Object[] args = new Object[]{params, d}; + return this.keySpecFactory.apply(args); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EdwardsPublicKeyDeriver.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EdwardsPublicKeyDeriver.java new file mode 100644 index 00000000..453a9d1b --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EdwardsPublicKeyDeriver.java @@ -0,0 +1,71 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; + +/** + * Derives a PublicKey from an Edwards-curve PrivateKey instance. + */ +final class EdwardsPublicKeyDeriver implements Function { + + public static final Function INSTANCE = new EdwardsPublicKeyDeriver(); + + private EdwardsPublicKeyDeriver() { + // prevent public instantiation. + } + + @Override + public PublicKey apply(PrivateKey privateKey) { + + EdwardsCurve curve = EdwardsCurve.findByKey(privateKey); + if (curve == null) { + String msg = "Unable to derive Edwards-curve PublicKey for specified PrivateKey: " + KeysBridge.toString(privateKey); + throw new UnsupportedKeyException(msg); + } + + byte[] pkBytes = curve.getKeyMaterial(privateKey); + + // This is a hack that utilizes the JCE implementations' behavior of using an RNG to generate a new private + // key, and from that, the implementation computes a public key from the private key bytes. + // Since we already have a private key, we provide a RNG that 'generates' the existing private key + // instead of a random one, and the corresponding public key will be computed for us automatically. + SecureRandom random = new ConstantRandom(pkBytes); + KeyPair pair = curve.keyPairBuilder().setRandom(random).build(); + Assert.stateNotNull(pair, "Edwards curve generated keypair cannot be null."); + return Assert.stateNotNull(pair.getPublic(), "Edwards curve KeyPair must have a PublicKey"); + } + + private static final class ConstantRandom extends SecureRandom { + private final byte[] value; + + public ConstantRandom(byte[] value) { + this.value = value.clone(); + } + + @Override + public void nextBytes(byte[] bytes) { + System.arraycopy(value, 0, bytes, 0, value.length); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java new file mode 100644 index 00000000..b9e88197 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/FamilyJwkFactory.java @@ -0,0 +1,28 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.security.Jwk; + +import java.security.Key; + +public interface FamilyJwkFactory> extends JwkFactory, Identifiable { + + boolean supports(Key key); + + boolean supports(JwkContext context); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java new file mode 100644 index 00000000..70c499e3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java @@ -0,0 +1,102 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.DecryptAeadRequest; +import io.jsonwebtoken.security.Message; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.spec.AlgorithmParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class GcmAesAeadAlgorithm extends AesAlgorithm implements AeadAlgorithm { + + private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding"; + + public GcmAesAeadAlgorithm(int keyLength) { + super("A" + keyLength + "GCM", TRANSFORMATION_STRING, keyLength); + } + + @Override + public AeadResult encrypt(final AeadRequest req) throws SecurityException { + + Assert.notNull(req, "Request cannot be null."); + final SecretKey key = assertKey(req.getKey()); + final byte[] plaintext = Assert.notEmpty(req.getPayload(), "Request content (plaintext) cannot be null or empty."); + final byte[] aad = getAAD(req); + final byte[] iv = ensureInitializationVector(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + byte[] taggedCiphertext = jca(req).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + if (Arrays.length(aad) > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(plaintext); + } + }); + + // When using GCM mode, the JDK appends the authentication tag to the ciphertext, so let's extract it: + // (tag has a length of BLOCK_SIZE_BITS): + int ciphertextLength = taggedCiphertext.length - BLOCK_BYTE_SIZE; + byte[] ciphertext = new byte[ciphertextLength]; + System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength); + byte[] tag = new byte[BLOCK_BYTE_SIZE]; + System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, BLOCK_BYTE_SIZE); + + return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(), ciphertext, key, aad, tag, iv); + } + + @Override + public Message decrypt(final DecryptAeadRequest req) throws SecurityException { + + Assert.notNull(req, "Request cannot be null."); + final SecretKey key = assertKey(req.getKey()); + final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request content (ciphertext) cannot be null or empty."); + final byte[] aad = getAAD(req); + final byte[] tag = Assert.notEmpty(req.getDigest(), "Decryption request authentication tag cannot be null or empty."); + final byte[] iv = assertDecryptionIv(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + //for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array: + final byte[] taggedCiphertext = Bytes.concat(ciphertext, tag); + + byte[] plaintext = jca(req).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + if (Arrays.length(aad) > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(taggedCiphertext); + } + }); + + return new DefaultMessage<>(plaintext); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java new file mode 100644 index 00000000..9ca92f56 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java @@ -0,0 +1,174 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.AeadRequest; +import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.DecryptAeadRequest; +import io.jsonwebtoken.security.Message; +import io.jsonwebtoken.security.SecretKeyBuilder; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SignatureException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class HmacAesAeadAlgorithm extends AesAlgorithm implements AeadAlgorithm { + + private static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding"; + + private final DefaultMacAlgorithm SIGALG; + + private static int digestLength(int keyLength) { + return keyLength * 2; + } + + private static String id(int keyLength) { + return "A" + keyLength + "CBC-HS" + digestLength(keyLength); + } + + public HmacAesAeadAlgorithm(String id, DefaultMacAlgorithm sigAlg) { + super(id, TRANSFORMATION_STRING, sigAlg.getKeyBitLength()); + this.SIGALG = sigAlg; + } + + public HmacAesAeadAlgorithm(int keyBitLength) { + this(id(keyBitLength), new DefaultMacAlgorithm(id(keyBitLength), "HmacSHA" + digestLength(keyBitLength), keyBitLength)); + } + + @Override + public int getKeyBitLength() { + return super.getKeyBitLength() * 2; + } + + @Override + public SecretKeyBuilder keyBuilder() { + // The Sun JCE KeyGenerator throws an exception if bitLengths are not standard AES 128, 192 or 256 values. + // Since the JWA HmacAes algorithms require double that, we use secure-random keys instead: + return new RandomSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength()); + } + + byte[] assertKeyBytes(SecureRequest request) { + SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); + return validateLength(key, this.keyBitLength * 2, true); + } + + @Override + public AeadResult encrypt(final AeadRequest req) { + + Assert.notNull(req, "Request cannot be null."); + + byte[] compositeKeyBytes = assertKeyBytes(req); + int halfCount = compositeKeyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 + byte[] macKeyBytes = Arrays.copyOfRange(compositeKeyBytes, 0, halfCount); + byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length); + final SecretKey encryptionKey = new SecretKeySpec(encKeyBytes, KEY_ALG_NAME); + + final byte[] plaintext = Assert.notEmpty(req.getPayload(), "Request content (plaintext) cannot be null or empty."); + final byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty + final byte[] iv = ensureInitializationVector(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + final byte[] ciphertext = jca(req).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec); + return cipher.doFinal(plaintext); + } + }); + + byte[] tag = sign(aad, iv, ciphertext, macKeyBytes); + + return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(), ciphertext, encryptionKey, aad, tag, iv); + } + + private byte[] sign(byte[] aad, byte[] iv, byte[] ciphertext, byte[] macKeyBytes) { + + long aadLength = io.jsonwebtoken.lang.Arrays.length(aad); + long aadLengthInBits = aadLength * Byte.SIZE; + long aadLengthInBitsAsUnsignedInt = aadLengthInBits & 0xffffffffL; + byte[] AL = Bytes.toBytes(aadLengthInBitsAsUnsignedInt); + + byte[] toHash = new byte[(int) aadLength + iv.length + ciphertext.length + AL.length]; + + if (aad != null) { + System.arraycopy(aad, 0, toHash, 0, aad.length); + System.arraycopy(iv, 0, toHash, aad.length, iv.length); + System.arraycopy(ciphertext, 0, toHash, aad.length + iv.length, ciphertext.length); + System.arraycopy(AL, 0, toHash, aad.length + iv.length + ciphertext.length, AL.length); + } else { + System.arraycopy(iv, 0, toHash, 0, iv.length); + System.arraycopy(ciphertext, 0, toHash, iv.length, ciphertext.length); + System.arraycopy(AL, 0, toHash, iv.length + ciphertext.length, AL.length); + } + + SecretKey key = new SecretKeySpec(macKeyBytes, SIGALG.getJcaName()); + SecureRequest request = + new DefaultSecureRequest<>(toHash, null, null, key); + byte[] digest = SIGALG.digest(request); + + // https://tools.ietf.org/html/rfc7518#section-5.2.2.1 #5 requires truncating the signature + // to be the same length as the macKey/encKey: + return assertTag(Arrays.copyOfRange(digest, 0, macKeyBytes.length)); + } + + @Override + public Message decrypt(final DecryptAeadRequest req) { + + Assert.notNull(req, "Request cannot be null."); + + byte[] compositeKeyBytes = assertKeyBytes(req); + int halfCount = compositeKeyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2 + byte[] macKeyBytes = Arrays.copyOfRange(compositeKeyBytes, 0, halfCount); + byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length); + final SecretKey decryptionKey = new SecretKeySpec(encKeyBytes, KEY_ALG_NAME); + + final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request content (ciphertext) cannot be null or empty."); + final byte[] aad = getAAD(req); + final byte[] tag = assertTag(req.getDigest()); + final byte[] iv = assertDecryptionIv(req); + final AlgorithmParameterSpec ivSpec = getIvSpec(iv); + + // Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided, + // thereby verifying none of it has been tampered with: + byte[] digest = sign(aad, iv, ciphertext, macKeyBytes); + if (!MessageDigest.isEqual(digest, tag)) { //constant time comparison to avoid side-channel attacks + String msg = "Ciphertext decryption failed: Authentication tag verification failed."; + throw new SignatureException(msg); + } + + byte[] plaintext = jca(req).withCipher(new CheckedFunction() { + @Override + public byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec); + return cipher.doFinal(ciphertext); + } + }); + + return new DefaultMessage<>(plaintext); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java new file mode 100644 index 00000000..7d9d110a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java @@ -0,0 +1,366 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.DefaultRegistry; +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Registry; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SignatureException; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.spec.AlgorithmParameterSpec; +import java.util.List; + +public class JcaTemplate { + + private static final List> FACTORIES = Collections.>of( + new CipherFactory(), + new KeyFactoryFactory(), + new SecretKeyFactoryFactory(), + new KeyGeneratorFactory(), + new KeyPairGeneratorFactory(), + new KeyAgreementFactory(), + new MessageDigestFactory(), + new SignatureFactory(), + new MacFactory(), + new AlgorithmParametersFactory(), + new CertificateFactoryFactory() + ); + + private static final Registry, InstanceFactory> REGISTRY = new DefaultRegistry<>( + "JCA Instance Factory", "instance class", FACTORIES, + new Function, Class>() { + @Override + public Class apply(InstanceFactory factory) { + return factory.getInstanceClass(); + } + }); + + private final String jcaName; + private final Provider provider; + private final SecureRandom secureRandom; + + JcaTemplate(String jcaName, Provider provider) { + this(jcaName, provider, null); + } + + JcaTemplate(String jcaName, Provider provider, SecureRandom secureRandom) { + this.jcaName = Assert.hasText(jcaName, "jcaName string cannot be null or empty."); + this.secureRandom = secureRandom != null ? secureRandom : Randoms.secureRandom(); + this.provider = provider; //may be null, meaning to use the JCA subsystem default provider + } + + private R execute(Class clazz, CheckedFunction fn) throws SecurityException { + InstanceFactory factory = REGISTRY.find(clazz); + Assert.notNull(factory, "Unsupported JCA instance class."); + return execute(factory, clazz, fn); + } + + public R withCipher(CheckedFunction fn) throws SecurityException { + return execute(Cipher.class, fn); + } + + public R withKeyFactory(CheckedFunction fn) throws SecurityException { + return execute(KeyFactory.class, fn); + } + + public R withSecretKeyFactory(CheckedFunction fn) throws SecurityException { + return execute(SecretKeyFactory.class, fn); + } + + public R withKeyGenerator(CheckedFunction fn) throws SecurityException { + return execute(KeyGenerator.class, fn); + } + + public R withKeyAgreement(CheckedFunction fn) throws SecurityException { + return execute(KeyAgreement.class, fn); + } + + public R withKeyPairGenerator(CheckedFunction fn) throws SecurityException { + return execute(KeyPairGenerator.class, fn); + } + + public R withMessageDigest(CheckedFunction fn) throws SecurityException { + return execute(MessageDigest.class, fn); + } + + public R withSignature(CheckedFunction fn) throws SecurityException { + return execute(Signature.class, fn); + } + + public R withMac(CheckedFunction fn) throws SecurityException { + return execute(Mac.class, fn); + } + + public R withAlgorithmParameters(CheckedFunction fn) throws SecurityException { + return execute(AlgorithmParameters.class, fn); + } + + public R withCertificateFactory(CheckedFunction fn) throws SecurityException { + return execute(CertificateFactory.class, fn); + } + + public SecretKey generateSecretKey(final int keyBitLength) { + return withKeyGenerator(new CheckedFunction() { + @Override + public SecretKey apply(KeyGenerator generator) { + generator.init(keyBitLength, secureRandom); + return generator.generateKey(); + } + }); + } + + public KeyPair generateKeyPair() { + return withKeyPairGenerator(new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator gen) { + return gen.generateKeyPair(); + } + }); + } + + public KeyPair generateKeyPair(final int keyBitLength) { + return withKeyPairGenerator(new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator generator) { + generator.initialize(keyBitLength, secureRandom); + return generator.generateKeyPair(); + } + }); + } + + public KeyPair generateKeyPair(final AlgorithmParameterSpec params) { + return withKeyPairGenerator(new CheckedFunction() { + @Override + public KeyPair apply(KeyPairGenerator generator) throws InvalidAlgorithmParameterException { + generator.initialize(params, secureRandom); + return generator.generateKeyPair(); + } + }); + } + + // protected visibility for testing + private R execute(InstanceFactory factory, Class clazz, CheckedFunction callback) throws SecurityException { + try { + Object object = factory.get(this.jcaName, this.provider); + T instance = Assert.isInstanceOf(clazz, object, "Factory instance does not match expected type."); + return callback.apply(instance); + } catch (SecurityException se) { + throw se; //propagate + } catch (Exception e) { + throw new SecurityException(factory.getId() + " callback execution failed: " + e.getMessage(), e); + } + } + + private interface InstanceFactory extends Identifiable { + + Class getInstanceClass(); + + T get(String jcaName, Provider provider) throws Exception; + } + + private static abstract class JcaInstanceFactory implements InstanceFactory { + + private final Class clazz; + + JcaInstanceFactory(Class clazz) { + this.clazz = Assert.notNull(clazz, "Class argument cannot be null."); + } + + @Override + public Class getInstanceClass() { + return this.clazz; + } + + @Override + public String getId() { + return clazz.getSimpleName(); + } + + @Override + public final T get(String jcaName, Provider provider) throws Exception { + Assert.hasText(jcaName, "jcaName cannot be null or empty."); + try { + return doGet(jcaName, provider); + } catch (Exception e) { + String msg = "Unable to obtain " + getId() + " instance from "; + if (provider != null) { + msg += "specified Provider '" + provider + "' "; + } else { + msg += "default JCA Provider "; + } + msg += "for JCA algorithm '" + jcaName + "': " + e.getMessage(); + throw wrap(msg, e); + } + } + + protected abstract T doGet(String jcaName, Provider provider) throws Exception; + + protected Exception wrap(String msg, Exception cause) { + if (Signature.class.isAssignableFrom(clazz) || Mac.class.isAssignableFrom(clazz)) { + return new SignatureException(msg, cause); + } + return new SecurityException(msg, cause); + } + } + + private static class CipherFactory extends JcaInstanceFactory { + CipherFactory() { + super(Cipher.class); + } + + @Override + public Cipher doGet(String jcaName, Provider provider) throws NoSuchPaddingException, NoSuchAlgorithmException { + return provider != null ? Cipher.getInstance(jcaName, provider) : Cipher.getInstance(jcaName); + } + } + + private static class KeyFactoryFactory extends JcaInstanceFactory { + KeyFactoryFactory() { + super(KeyFactory.class); + } + + @Override + public KeyFactory doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? KeyFactory.getInstance(jcaName, provider) : KeyFactory.getInstance(jcaName); + } + } + + private static class SecretKeyFactoryFactory extends JcaInstanceFactory { + SecretKeyFactoryFactory() { + super(SecretKeyFactory.class); + } + + @Override + public SecretKeyFactory doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? SecretKeyFactory.getInstance(jcaName, provider) : SecretKeyFactory.getInstance(jcaName); + } + } + + private static class KeyGeneratorFactory extends JcaInstanceFactory { + KeyGeneratorFactory() { + super(KeyGenerator.class); + } + + @Override + public KeyGenerator doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? KeyGenerator.getInstance(jcaName, provider) : KeyGenerator.getInstance(jcaName); + } + } + + private static class KeyPairGeneratorFactory extends JcaInstanceFactory { + KeyPairGeneratorFactory() { + super(KeyPairGenerator.class); + } + + @Override + public KeyPairGenerator doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? KeyPairGenerator.getInstance(jcaName, provider) : KeyPairGenerator.getInstance(jcaName); + } + } + + private static class KeyAgreementFactory extends JcaInstanceFactory { + KeyAgreementFactory() { + super(KeyAgreement.class); + } + + @Override + public KeyAgreement doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? KeyAgreement.getInstance(jcaName, provider) : KeyAgreement.getInstance(jcaName); + } + } + + private static class MessageDigestFactory extends JcaInstanceFactory { + MessageDigestFactory() { + super(MessageDigest.class); + } + + @Override + public MessageDigest doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? MessageDigest.getInstance(jcaName, provider) : MessageDigest.getInstance(jcaName); + } + } + + private static class SignatureFactory extends JcaInstanceFactory { + SignatureFactory() { + super(Signature.class); + } + + @Override + public Signature doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? Signature.getInstance(jcaName, provider) : Signature.getInstance(jcaName); + } + } + + private static class MacFactory extends JcaInstanceFactory { + MacFactory() { + super(Mac.class); + } + + @Override + public Mac doGet(String jcaName, Provider provider) throws NoSuchAlgorithmException { + return provider != null ? Mac.getInstance(jcaName, provider) : Mac.getInstance(jcaName); + } + } + + private static class AlgorithmParametersFactory extends JcaInstanceFactory { + AlgorithmParametersFactory() { + super(AlgorithmParameters.class); + } + + @Override + protected AlgorithmParameters doGet(String jcaName, Provider provider) throws Exception { + return provider != null ? + AlgorithmParameters.getInstance(jcaName, provider) : + AlgorithmParameters.getInstance(jcaName); + } + } + + private static class CertificateFactoryFactory extends JcaInstanceFactory { + CertificateFactoryFactory() { + super(CertificateFactory.class); + } + + @Override + protected CertificateFactory doGet(String jcaName, Provider provider) throws Exception { + return provider != null ? + CertificateFactory.getInstance(jcaName, provider) : + CertificateFactory.getInstance(jcaName); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java new file mode 100644 index 00000000..60144942 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -0,0 +1,82 @@ +/* + * 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.security; + +import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.Nameable; +import io.jsonwebtoken.security.HashAlgorithm; +import io.jsonwebtoken.security.X509Mutator; + +import java.net.URI; +import java.security.Key; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface JwkContext extends Identifiable, Map, FieldReadable, Nameable, + X509Mutator> { + + JwkContext setId(String id); + + JwkContext setIdThumbprintAlgorithm(HashAlgorithm alg); + + HashAlgorithm getIdThumbprintAlgorithm(); + + String getType(); + + JwkContext setType(String type); + + Set getOperations(); + + JwkContext setOperations(Set operations); + + String getAlgorithm(); + + JwkContext setAlgorithm(String algorithm); + + String getPublicKeyUse(); + + JwkContext setPublicKeyUse(String use); + + URI getX509Url(); + + List getX509CertificateChain(); + + byte[] getX509CertificateSha1Thumbprint(); + + byte[] getX509CertificateSha256Thumbprint(); + + K getKey(); + + JwkContext setKey(K key); + + PublicKey getPublicKey(); + + JwkContext setPublicKey(PublicKey publicKey); + + Provider getProvider(); + + JwkContext setProvider(Provider provider); + + SecureRandom getRandom(); + + JwkContext setRandom(SecureRandom random); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java new file mode 100644 index 00000000..5ddef701 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java @@ -0,0 +1,133 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Nameable; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkBuilder; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.OctetPrivateJwk; +import io.jsonwebtoken.security.OctetPublicJwk; +import io.jsonwebtoken.security.PrivateJwk; +import io.jsonwebtoken.security.PublicJwk; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.SecretJwk; + +import java.util.Map; + +import static io.jsonwebtoken.lang.Strings.nespace; + +public final class JwkConverter> implements Converter { + + @SuppressWarnings("unchecked") + public static final JwkConverter> PUBLIC_JWK = new JwkConverter<>((Class>) (Class) PublicJwk.class); + + private final Class desiredType; + + public JwkConverter(Class desiredType) { + this.desiredType = Assert.notNull(desiredType, "desiredType cannot be null."); + } + + @Override + public Object applyTo(T jwk) { + return desiredType.cast(jwk); + } + + private static String articleFor(String s) { + switch (s.charAt(0)) { + case 'E': // for Elliptic/Edwards Curve + case 'R': // for RSA + return "an"; + default: + return "a"; + } + } + + private static String typeString(Jwk jwk) { + Assert.isInstanceOf(Nameable.class, jwk, "All JWK implementations must implement Nameable."); + return ((Nameable)jwk).getName(); + } + + private static String typeString(Class clazz) { + StringBuilder sb = new StringBuilder(); + if (SecretJwk.class.isAssignableFrom(clazz)) { + sb.append("Secret"); + } else if (RsaPublicJwk.class.isAssignableFrom(clazz) || RsaPrivateJwk.class.isAssignableFrom(clazz)) { + sb.append("RSA"); + } else if (EcPublicJwk.class.isAssignableFrom(clazz) || EcPrivateJwk.class.isAssignableFrom(clazz)) { + sb.append("EC"); + } else if (OctetPublicJwk.class.isAssignableFrom(clazz) || OctetPrivateJwk.class.isAssignableFrom(clazz)) { + sb.append("Edwards Curve"); + } + return typeString(sb, clazz); + } + + private static String typeString(StringBuilder sb, Class clazz) { + if (PublicJwk.class.isAssignableFrom(clazz)) { + nespace(sb).append("Public"); + } else if (PrivateJwk.class.isAssignableFrom(clazz)) { + nespace(sb).append("Private"); + } + nespace(sb).append("JWK"); + return sb.toString(); + } + + private IllegalArgumentException unexpectedIAE(Jwk jwk) { + String desired = typeString(this.desiredType); + String jwkType = typeString(jwk); + String msg = "Value must be " + articleFor(desired) + " " + desired + ", not " + + articleFor(jwkType) + " " + jwkType + "."; + return new IllegalArgumentException(msg); + } + + @Override + public T applyFrom(Object o) { + Assert.notNull(o, "JWK argument cannot be null."); + if (desiredType.isInstance(o)) { + return desiredType.cast(o); + } else if (o instanceof Jwk) { + throw unexpectedIAE((Jwk) o); + } + if (!(o instanceof Map)) { + String msg = "Value must be a Jwk or Map. Type found: " + o.getClass().getName() + "."; + throw new IllegalArgumentException(msg); + } + Map map = (Map) o; + JwkBuilder builder = Jwks.builder(); + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Assert.notNull(key, "JWK map key cannot be null."); + if (!(key instanceof String)) { + String msg = "JWK map keys must be Strings. Encountered key '" + key + "' of type " + + key.getClass().getName() + "."; + throw new IllegalArgumentException(msg); + } + String skey = (String) key; + builder.put(skey, entry.getValue()); + } + + Jwk jwk = builder.build(); + if (desiredType.isInstance(jwk)) { + return desiredType.cast(jwk); + } + throw unexpectedIAE(jwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java similarity index 68% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java rename to impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java index 1e84b620..322d0876 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * 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. @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Jwk; import java.security.Key; -public interface SignatureValidatorFactory { +public interface JwkFactory> { - SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key); + JwkContext newContext(JwkContext src, K key); + + J createJwk(JwkContext ctx); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java new file mode 100644 index 00000000..9d1e5c91 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwtX509StringConverter.java @@ -0,0 +1,75 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public class JwtX509StringConverter implements Converter { + + public static final JwtX509StringConverter INSTANCE = new JwtX509StringConverter(); + + // Returns a Base64 encoded (NOT Base64Url encoded) string of the cert's encoded byte array per + // https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.6 + // https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.8 + // https://www.rfc-editor.org/rfc/rfc7517.html#section-4.7 + @Override + public String applyTo(X509Certificate cert) { + Assert.notNull(cert, "X509Certificate cannot be null."); + byte[] der; + try { + der = cert.getEncoded(); + } catch (CertificateEncodingException e) { + String msg = "Unable to access X509Certificate encoded bytes necessary to perform DER " + + "Base64-encoding. Certificate: {" + cert + "}. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + if (Arrays.length(der) == 0) { + String msg = "X509Certificate encoded bytes cannot be null or empty. Certificate: {" + cert + "}."; + throw new IllegalArgumentException(msg); + } + return Encoders.BASE64.encode(der); + } + + //visible for testing + protected CertificateFactory newCertificateFactory() throws CertificateException { + return CertificateFactory.getInstance("X.509"); + } + + @Override + public X509Certificate applyFrom(String s) { + Assert.hasText(s, "X.509 Certificate encoded string cannot be null or empty."); + try { + byte[] der = Decoders.BASE64.decode(s); //RFC requires Base64, not Base64Url + CertificateFactory cf = newCertificateFactory(); + InputStream stream = new ByteArrayInputStream(der); + return (X509Certificate) cf.generateCertificate(stream); + } catch (Exception e) { + String msg = "Unable to convert Base64 String '" + s + "' to X509Certificate instance. Cause: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java new file mode 100644 index 00000000..d2a51766 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyPairs.java @@ -0,0 +1,61 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; + +public final class KeyPairs { + + private KeyPairs() { + } + + private static String familyPrefix(Class clazz) { + if (RSAKey.class.isAssignableFrom(clazz)) { + return "RSA "; + } else if (ECKey.class.isAssignableFrom(clazz)) { + return "EC "; + } else { + return Strings.EMPTY; + } + } + + public static K getKey(KeyPair pair, Class clazz) { + Assert.notNull(pair, "KeyPair cannot be null."); + String prefix = familyPrefix(clazz) + "KeyPair "; + boolean isPrivate = PrivateKey.class.isAssignableFrom(clazz); + Key key = isPrivate ? pair.getPrivate() : pair.getPublic(); + return assertKey(key, clazz, prefix); + } + + public static K assertKey(Key key, Class clazz, String msgPrefix) { + Assert.notNull(key, "Key argument cannot be null."); + Assert.notNull(clazz, "Class argument cannot be null."); + String type = key instanceof PrivateKey ? "private" : "public"; + if (!clazz.isInstance(key)) { + String msg = msgPrefix + type + " key must be an instance of " + clazz.getName() + + ". Type found: " + key.getClass().getName(); + throw new IllegalArgumentException(msg); + } + return clazz.cast(key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java new file mode 100644 index 00000000..fbd76a5c --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUsage.java @@ -0,0 +1,79 @@ +/* + * 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.security; + +import java.security.cert.X509Certificate; + +public final class KeyUsage { + + private static final boolean[] NO_FLAGS = new boolean[9]; + + // Direct from X509Certificate#getKeyUsage() JavaDoc. For an understand of when/how to use these + // flags, read https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 + private static final int + digitalSignature = 0, + nonRepudiation = 1, + keyEncipherment = 2, + dataEncipherment = 3, + keyAgreement = 4, + keyCertSign = 5, + cRLSign = 6, + encipherOnly = 7, //if keyAgreement, then only encipher data during key agreement + decipherOnly = 8; //if keyAgreement, then only decipher data during key agreement + + private final boolean[] is; //for readability: i.e. is[nonRepudiation] simulates isNonRepudiation, etc. + + public KeyUsage(X509Certificate cert) { + boolean[] arr = cert != null ? cert.getKeyUsage() : NO_FLAGS; + this.is = arr != null ? arr : NO_FLAGS; + } + + public boolean isDigitalSignature() { + return is[digitalSignature]; + } + + public boolean isNonRepudiation() { + return is[nonRepudiation]; + } + + public boolean isKeyEncipherment() { + return is[keyEncipherment]; + } + + public boolean isDataEncipherment() { + return is[dataEncipherment]; + } + + public boolean isKeyAgreement() { + return is[keyAgreement]; + } + + public boolean isKeyCertSign() { + return is[keyCertSign]; + } + + public boolean isCRLSign() { + return is[cRLSign]; + } + + public boolean isEncipherOnly() { + return is[encipherOnly]; + } + + public boolean isDecipherOnly() { + return is[decipherOnly]; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java new file mode 100644 index 00000000..9363dbb6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyUseStrategy.java @@ -0,0 +1,23 @@ +/* + * 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.security; + +//TODO: Make a non-impl concept? +public interface KeyUseStrategy { + + //TODO: change argument to have more information? + String toJwkValue(KeyUsage keyUses); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java new file mode 100644 index 00000000..de7d5562 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -0,0 +1,69 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.Key; +import java.security.PublicKey; + +@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation +public final class KeysBridge { + + // prevent instantiation + private KeysBridge() { + } + + public static Password forPassword(char[] password) { + return new PasswordSpec(password); + } + + public static byte[] findEncoded(Key key) { + Assert.notNull(key, "Key cannot be null."); + byte[] encoded = null; + try { + encoded = key.getEncoded(); + } catch (Throwable ignored) { + } + return encoded; + } + + public static byte[] getEncoded(Key key) { + Assert.notNull(key, "Key cannot be null."); + byte[] encoded = findEncoded(key); + if (Bytes.isEmpty(encoded)) { + String msg = key.getClass().getName() + " encoded bytes cannot be null or empty."; + throw new UnsupportedKeyException(msg); + } + return encoded; + } + + public static String toString(Key key) { + if (key == null) { + return "null"; + } + if (key instanceof PublicKey) { + return key.toString(); // safe to show internal key state as it's a public key + } + // else secret or private key, don't show internal key state, just public attributes + return "class: " + key.getClass().getName() + + ", algorithm: " + key.getAlgorithm() + + ", format: " + key.getFormat(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java b/impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java new file mode 100644 index 00000000..27351f03 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/LocatingKeyResolver.java @@ -0,0 +1,44 @@ +/* + * 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.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.SigningKeyResolver; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +@SuppressWarnings("deprecation") // TODO: delete this class for 1.0 +public class LocatingKeyResolver implements SigningKeyResolver { + + private final Locator locator; + + public LocatingKeyResolver(Locator locator) { + this.locator = Assert.notNull(locator, "Locator cannot be null."); + } + + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + return this.locator.locate(header); + } + + @Override + public Key resolveSigningKey(JwsHeader header, byte[] content) { + return this.locator.locate(header); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/NamedParameterSpecValueFinder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/NamedParameterSpecValueFinder.java new file mode 100644 index 00000000..c5b35353 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/NamedParameterSpecValueFinder.java @@ -0,0 +1,40 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Function; +import io.jsonwebtoken.impl.lang.Functions; +import io.jsonwebtoken.impl.lang.OptionalMethodInvoker; + +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; + +public class NamedParameterSpecValueFinder implements Function { + + private static final Function EDEC_KEY_GET_PARAMS = + new OptionalMethodInvoker<>("java.security.interfaces.EdECKey", "getParams"); + private static final Function XEC_KEY_GET_PARAMS = + new OptionalMethodInvoker<>("java.security.interfaces.XECKey", "getParams"); + private static final Function GET_NAME = + new OptionalMethodInvoker<>("java.security.spec.NamedParameterSpec", "getName"); + + private static final Function COMPOSED = Functions.andThen(Functions.firstResult(EDEC_KEY_GET_PARAMS, XEC_KEY_GET_PARAMS), GET_NAME); + + @Override + public String apply(final Key key) { + return COMPOSED.apply(key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java new file mode 100644 index 00000000..79a95cd8 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/NoneSignatureAlgorithm.java @@ -0,0 +1,61 @@ +/* + * 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.security; + +import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.VerifySecureDigestRequest; + +import java.security.Key; + +public class NoneSignatureAlgorithm implements SecureDigestAlgorithm { + + private static final String ID = "none"; + + @Override + public String getId() { + return ID; + } + + @Override + public byte[] digest(SecureRequest request) throws SecurityException { + throw new SignatureException("The 'none' algorithm cannot be used to create signatures."); + } + + @Override + public boolean verify(VerifySecureDigestRequest request) throws SignatureException { + throw new SignatureException("The 'none' algorithm cannot be used to verify signatures."); + } + + @Override + public boolean equals(Object obj) { + return this == obj || + (obj instanceof SecureDigestAlgorithm && + ID.equalsIgnoreCase(((SecureDigestAlgorithm) obj).getId())); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public String toString() { + return ID; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/OctetJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/OctetJwkFactory.java new file mode 100644 index 00000000..65589608 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/OctetJwkFactory.java @@ -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.security; + +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.security.Key; +import java.util.Set; + +public abstract class OctetJwkFactory> extends AbstractFamilyJwkFactory { + + OctetJwkFactory(Class keyType, Set> fields) { + super(DefaultOctetPublicJwk.TYPE_VALUE, keyType, fields); + } + + @Override + public boolean supports(Key key) { + return super.supports(key) && EdwardsCurve.isEdwards(key); + } + + protected EdwardsCurve getCurve(final FieldReadable reader) throws UnsupportedKeyException { + Field field = DefaultOctetPublicJwk.CRV; + String crvId = reader.get(field); + EdwardsCurve curve = EdwardsCurve.findById(crvId); + if (curve == null) { + String msg = "Unrecognized OKP JWK " + field + " value '" + crvId + "'"; + throw new UnsupportedKeyException(msg); + } + return curve; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/OctetPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/OctetPrivateJwkFactory.java new file mode 100644 index 00000000..ba12acc6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/OctetPrivateJwkFactory.java @@ -0,0 +1,90 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.OctetPrivateJwk; +import io.jsonwebtoken.security.OctetPublicJwk; + +import java.security.PrivateKey; +import java.security.PublicKey; + +public class OctetPrivateJwkFactory extends OctetJwkFactory> { + + public OctetPrivateJwkFactory() { + super(PrivateKey.class, DefaultOctetPrivateJwk.FIELDS); + } + + @Override + protected boolean supportsKeyValues(JwkContext ctx) { + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultOctetPrivateJwk.D.getId()); + } + + @Override + protected OctetPrivateJwk createJwkFromKey(JwkContext ctx) { + PrivateKey key = Assert.notNull(ctx.getKey(), "PrivateKey cannot be null."); + EdwardsCurve crv = EdwardsCurve.forKey(key); + + PublicKey pub = ctx.getPublicKey(); + if (pub != null) { + if (!crv.equals(EdwardsCurve.forKey(pub))) { + String msg = "Specified Edwards Curve PublicKey does not match the specified PrivateKey's curve."; + throw new InvalidKeyException(msg); + } + } else { // not supplied - try to generate it: + pub = EdwardsCurve.derivePublic(key); + } + + // If a JWK fingerprint has been requested to be the JWK id, ensure we copy over the one computed for the + // public key per https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + boolean copyId = !Strings.hasText(ctx.getId()) && ctx.getIdThumbprintAlgorithm() != null; + JwkContext pubCtx = OctetPublicJwkFactory.INSTANCE.newContext(ctx, pub); + OctetPublicJwk pubJwk = OctetPublicJwkFactory.INSTANCE.createJwk(pubCtx); + ctx.putAll(pubJwk); + if (copyId) { + ctx.setId(pubJwk.getId()); + } + + //now add the d value + byte[] d = crv.getKeyMaterial(key); + Assert.notEmpty(d, "Edwards PrivateKey 'd' value cannot be null or empty."); + //TODO: assert that the curve contains the specified key + put(ctx, DefaultOctetPrivateJwk.D, d); + + return new DefaultOctetPrivateJwk<>(ctx, pubJwk); + } + + @Override + protected OctetPrivateJwk createJwkFromValues(JwkContext ctx) { + FieldReadable reader = new RequiredFieldReader(ctx); + EdwardsCurve curve = getCurve(reader); + //TODO: assert that the curve contains the specified key + + // public values are required, so assert them: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultOctetPublicJwk.FIELDS, ctx); + OctetPublicJwk pubJwk = OctetPublicJwkFactory.INSTANCE.createJwkFromValues(pubCtx); + + byte[] d = reader.get(DefaultOctetPrivateJwk.D); + PrivateKey key = curve.toPrivateKey(d, ctx.getProvider()); + ctx.setKey(key); + + return new DefaultOctetPrivateJwk<>(ctx, pubJwk); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/OctetPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/OctetPublicJwkFactory.java new file mode 100644 index 00000000..7b940827 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/OctetPublicJwkFactory.java @@ -0,0 +1,55 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.OctetPublicJwk; + +import java.security.PublicKey; + +public class OctetPublicJwkFactory extends OctetJwkFactory> { + + static final OctetPublicJwkFactory INSTANCE = new OctetPublicJwkFactory(); + + OctetPublicJwkFactory() { + super(PublicKey.class, DefaultOctetPublicJwk.FIELDS); + } + + @Override + protected OctetPublicJwk createJwkFromKey(JwkContext ctx) { + PublicKey key = Assert.notNull(ctx.getKey(), "PublicKey cannot be null."); + EdwardsCurve crv = EdwardsCurve.forKey(key); + byte[] x = crv.getKeyMaterial(key); + Assert.notEmpty(x, "Edwards PublicKey 'x' value cannot be null or empty."); + //TODO: assert that the curve contains the specified key + put(ctx, DefaultOctetPublicJwk.CRV, crv.getId()); + put(ctx, DefaultOctetPublicJwk.X, x); + return new DefaultOctetPublicJwk<>(ctx); + } + + @Override + protected OctetPublicJwk createJwkFromValues(JwkContext ctx) { + FieldReadable reader = new RequiredFieldReader(ctx); + EdwardsCurve curve = getCurve(reader); + byte[] x = reader.get(DefaultOctetPublicJwk.X); + //TODO: assert that the curve contains the specified key + PublicKey key = curve.toPublicKey(x, ctx.getProvider()); + ctx.setKey(key); + return new DefaultOctetPublicJwk<>(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/PasswordSpec.java b/impl/src/main/java/io/jsonwebtoken/impl/security/PasswordSpec.java new file mode 100644 index 00000000..ae970478 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/PasswordSpec.java @@ -0,0 +1,100 @@ +/* + * 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.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.security.Password; + +import java.security.spec.KeySpec; + +public class PasswordSpec implements Password, KeySpec { + + private static final String NONE_ALGORITHM = "NONE"; + private static final String DESTROYED_MSG = "Password has been destroyed. Password character array may not be obtained."; + private static final String ENCODED_DISABLED_MSG = + "getEncoded() is disabled for Password instances as they are intended to be used " + + "with key derivation algorithms only. Because passwords rarely have the length or entropy " + + "necessary for secure cryptographic operations such as authenticated hashing or encryption, " + + "they are disabled as direct inputs for these operations to help avoid accidental misuse; if " + + "you see this exception message, it is likely that the associated Password instance is " + + "being used incorrectly."; + + private volatile boolean destroyed; + private final char[] password; + + public PasswordSpec(char[] password) { + this.password = Assert.notEmpty(password, "Password character array cannot be null or empty."); + } + + private void assertActive() { + if (destroyed) { + throw new IllegalStateException(DESTROYED_MSG); + } + } + + @Override + public char[] toCharArray() { + assertActive(); + return this.password.clone(); + } + + @Override + public String getAlgorithm() { + return NONE_ALGORITHM; + } + + @Override + public String getFormat() { + return null; // encoding isn't supported, so we return null per the Key#getFormat() JavaDoc + } + + @Override + public byte[] getEncoded() { + throw new UnsupportedOperationException(ENCODED_DISABLED_MSG); + } + + public void destroy() { + this.destroyed = true; + java.util.Arrays.fill(password, '\u0000'); + } + + public boolean isDestroyed() { + return this.destroyed; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(this.password); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof PasswordSpec) { + PasswordSpec other = (PasswordSpec) obj; + return Objects.nullSafeEquals(this.password, other.password); + } + return false; + } + + @Override + public final String toString() { + return ""; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java new file mode 100644 index 00000000..64103fdf --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -0,0 +1,209 @@ +/* + * 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.security; + +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.impl.DefaultJweHeader; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.DecryptionKeyRequest; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.SecurityException; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { + + // See https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 : + private static final int DEFAULT_SHA256_ITERATIONS = 310000; + private static final int DEFAULT_SHA384_ITERATIONS = 210000; + private static final int DEFAULT_SHA512_ITERATIONS = 120000; + + private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2 + private static final String MIN_ITERATIONS_MSG_PREFIX = + "[JWA RFC 7518, Section 4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2) " + + "recommends password-based-encryption iterations be greater than or equal to " + + MIN_RECOMMENDED_ITERATIONS + ". Provided: "; + + private final int HASH_BYTE_LENGTH; + private final int DERIVED_KEY_BIT_LENGTH; + private final byte[] SALT_PREFIX; + private final int DEFAULT_ITERATIONS; + private final KeyAlgorithm wrapAlg; + + private static byte[] toRfcSaltPrefix(byte[] bytes) { + // last byte must always be zero as it is a delimiter per + // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.1 + // We ensure this by creating a byte array that is one element larger than bytes.length since Java defaults all + // new byte array indices to 0x00, meaning the last one will be our zero delimiter: + byte[] output = new byte[bytes.length + 1]; + System.arraycopy(bytes, 0, output, 0, bytes.length); + return output; + } + + private static int hashBitLength(int keyBitLength) { + return keyBitLength * 2; + } + + private static String idFor(int hashBitLength, KeyAlgorithm wrapAlg) { + Assert.notNull(wrapAlg, "wrapAlg argument cannot be null."); + return "PBES2-HS" + hashBitLength + "+" + wrapAlg.getId(); + } + + public static int assertIterations(int iterations) { + if (iterations < MIN_RECOMMENDED_ITERATIONS) { + String msg = MIN_ITERATIONS_MSG_PREFIX + iterations; + throw new IllegalArgumentException(msg); + } + return iterations; + } + + public Pbes2HsAkwAlgorithm(int keyBitLength) { + this(hashBitLength(keyBitLength), new AesWrapKeyAlgorithm(keyBitLength)); + } + + protected Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm wrapAlg) { + super(idFor(hashBitLength, wrapAlg), "PBKDF2WithHmacSHA" + hashBitLength); + this.wrapAlg = wrapAlg; // no need to assert non-null due to 'idFor' implementation above + + // There's some white box knowledge here: there is no need to assert the value of hashBitLength + // because that is done implicitly in the constructor when instantiating AesWrapKeyAlgorithm. See that class's + // implementation to see the assertion: + this.HASH_BYTE_LENGTH = hashBitLength / Byte.SIZE; + + // If the JwtBuilder caller doesn't specify an iteration count, fall back to OWASP best-practice recommendations + // per https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 + if (hashBitLength >= 512) { + DEFAULT_ITERATIONS = DEFAULT_SHA512_ITERATIONS; + } else if (hashBitLength >= 384) { + DEFAULT_ITERATIONS = DEFAULT_SHA384_ITERATIONS; + } else { + DEFAULT_ITERATIONS = DEFAULT_SHA256_ITERATIONS; + } + + // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8, 2nd paragraph, last sentence: + // "Their derived-key lengths respectively are 16, 24, and 32 octets." : + this.DERIVED_KEY_BIT_LENGTH = hashBitLength / 2; // results in 128, 192, or 256 + + this.SALT_PREFIX = toRfcSaltPrefix(getId().getBytes(StandardCharsets.UTF_8)); + + // PBKDF2WithHmacSHA* algorithms are only available on JDK 8 and later, so enable BC as a backup provider if + // necessary for <= JDK 7: + // TODO: remove when dropping Java 7 support: + setProvider(Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + public SecretKeyFactory get() throws Exception { + return SecretKeyFactory.getInstance(getJcaName()); + } + }))); + } + + // protected visibility for testing + protected SecretKey deriveKey(SecretKeyFactory factory, final char[] password, final byte[] rfcSalt, int iterations) throws Exception { + PBEKeySpec spec = new PBEKeySpec(password, rfcSalt, iterations, DERIVED_KEY_BIT_LENGTH); + try { + SecretKey derived = factory.generateSecret(spec); + return new SecretKeySpec(derived.getEncoded(), AesAlgorithm.KEY_ALG_NAME); // needed to keep the Sun Provider happy + } finally { + spec.clearPassword(); + } + } + + private SecretKey deriveKey(final KeyRequest request, final char[] password, final byte[] salt, final int iterations) { + try { + Assert.notEmpty(password, "Key password character array cannot be null or empty."); + return jca(request).withSecretKeyFactory(new CheckedFunction() { + @Override + public SecretKey apply(SecretKeyFactory factory) throws Exception { + return deriveKey(factory, password, salt, iterations); + } + }); + } finally { + java.util.Arrays.fill(password, '\u0000'); + } + } + + protected byte[] generateInputSalt(KeyRequest request) { + byte[] inputSalt = new byte[this.HASH_BYTE_LENGTH]; + ensureSecureRandom(request).nextBytes(inputSalt); + return inputSalt; + } + + // protected visibility for testing + protected byte[] toRfcSalt(byte[] inputSalt) { + return Bytes.concat(this.SALT_PREFIX, inputSalt); + } + + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + + Assert.notNull(request, "request cannot be null."); + final Password key = Assert.notNull(request.getPayload(), "Encryption Password cannot be null."); + Integer p2c = request.getHeader().getPbes2Count(); + if (p2c == null) { + p2c = DEFAULT_ITERATIONS; + request.getHeader().setPbes2Count(p2c); + } + final int iterations = assertIterations(p2c); + byte[] inputSalt = generateInputSalt(request); + final byte[] rfcSalt = toRfcSalt(inputSalt); + char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next: + final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); + + // now get a new CEK that is encrypted ('wrapped') with the PBE-derived key: + DefaultKeyRequest wrapReq = new DefaultKeyRequest<>(derivedKek, request.getProvider(), + request.getSecureRandom(), request.getHeader(), request.getEncryptionAlgorithm()); + KeyResult result = wrapAlg.getEncryptionKey(wrapReq); + + request.getHeader().put(DefaultJweHeader.P2S.getId(), inputSalt); //retain for recipients + + return result; + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + + JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); + final Password key = Assert.notNull(request.getKey(), "Decryption Password cannot be null."); + FieldReadable reader = new RequiredFieldReader(header); + final byte[] inputSalt = reader.get(DefaultJweHeader.P2S); + final int iterations = reader.get(DefaultJweHeader.P2C); + final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt); + final char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next: + final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); + + DecryptionKeyRequest unwrapReq = + new DefaultDecryptionKeyRequest<>(request.getPayload(), request.getProvider(), + request.getSecureRandom(), header, request.getEncryptionAlgorithm(), derivedKek); + + return wrapAlg.getDecryptionKey(unwrapReq); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java new file mode 100644 index 00000000..ed02a992 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Providers.java @@ -0,0 +1,85 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.Condition; +import io.jsonwebtoken.lang.Classes; + +import java.security.Provider; +import java.security.Security; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @since JJWT_RELEASE_VERSION + */ +final class Providers { + + private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider"; + static final boolean BOUNCY_CASTLE_AVAILABLE = Classes.isAvailable(BC_PROVIDER_CLASS_NAME); + private static final AtomicReference BC_PROVIDER = new AtomicReference<>(); + + private Providers() { + } + + private static Provider findBouncyCastle() { + if (!BOUNCY_CASTLE_AVAILABLE) { + return null; + } + Provider provider = BC_PROVIDER.get(); + if (provider == null) { + + Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); + + //check to see if the user has already registered the BC provider: + Provider[] providers = Security.getProviders(); + for (Provider aProvider : providers) { + if (clazz.isInstance(aProvider)) { + BC_PROVIDER.set(aProvider); + return aProvider; + } + } + + //user hasn't created the BC provider, so we'll create one just for JJWT's needs: + provider = Classes.newInstance(clazz); + BC_PROVIDER.set(provider); + } + return provider; + } + + /** + * Returns the BouncyCastle provider if and only if the specified Condition evaluates to {@code true} + * and BouncyCastle is available. Returns {@code null} otherwise. + * + *

    If the condition evaluates to true and the JVM runtime already has BouncyCastle registered + * (e.g. {@code Security.addProvider(bcProvider)}, that Provider instance will be found and returned. + * If an existing BC provider is not found, a new BC instance will be created, cached for future reference, + * and returned.

    + * + *

    If a new BC provider is created and returned, it is not registered in the JVM via + * {@code Security.addProvider} to ensure JJWT doesn't interfere with the application security provider + * configuration and/or expectations.

    + * + * @param c condition to evaluate + * @return any available BouncyCastle Provider if {@code c} evaluates to true, or {@code null} if either + * {@code c} evaluates to false, or BouncyCastle is not available. + */ + public static Provider findBouncyCastle(Condition c) { + if (c.test()) { + return findBouncyCastle(); + } + return null; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java new file mode 100644 index 00000000..1d35109e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverter.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 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.security; + +import io.jsonwebtoken.impl.lang.Converter; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.math.BigInteger; +import java.security.spec.RSAOtherPrimeInfo; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +class RSAOtherPrimeInfoConverter implements Converter { + + static final RSAOtherPrimeInfoConverter INSTANCE = new RSAOtherPrimeInfoConverter(); + + static final Field PRIME_FACTOR = Fields.secretBigInt("r", "Prime Factor"); + static final Field FACTOR_CRT_EXPONENT = Fields.secretBigInt("d", "Factor CRT Exponent"); + static final Field FACTOR_CRT_COEFFICIENT = Fields.secretBigInt("t", "Factor CRT Coefficient"); + static final Set> FIELDS = Collections.>setOf(PRIME_FACTOR, FACTOR_CRT_EXPONENT, FACTOR_CRT_COEFFICIENT); + + @Override + public Object applyTo(RSAOtherPrimeInfo info) { + Map m = new LinkedHashMap<>(3); + m.put(PRIME_FACTOR.getId(), PRIME_FACTOR.applyTo(info.getPrime())); + m.put(FACTOR_CRT_EXPONENT.getId(), FACTOR_CRT_EXPONENT.applyTo(info.getExponent())); + m.put(FACTOR_CRT_COEFFICIENT.getId(), FACTOR_CRT_COEFFICIENT.applyTo(info.getCrtCoefficient())); + return m; + } + + @Override + public RSAOtherPrimeInfo applyFrom(Object o) { + if (o == null) { + throw new MalformedKeyException("RSA JWK 'oth' (Other Prime Info) element cannot be null."); + } + if (!(o instanceof Map)) { + String msg = "RSA JWK 'oth' (Other Prime Info) must contain map elements of name/value pairs. " + + "Element type found: " + o.getClass().getName(); + throw new MalformedKeyException(msg); + } + Map m = (Map) o; + if (Collections.isEmpty(m)) { + throw new MalformedKeyException("RSA JWK 'oth' (Other Prime Info) element map cannot be empty."); + } + + // Need a Context instance to satisfy the API contract of the reader.get* methods below. + JwkContext ctx = new DefaultJwkContext<>(FIELDS); + try { + for (Map.Entry entry : m.entrySet()) { + String name = String.valueOf(entry.getKey()); + ctx.put(name, entry.getValue()); + } + } catch (Exception e) { + throw new MalformedKeyException(e.getMessage(), e); + } + + FieldReadable reader = new RequiredFieldReader(ctx); + BigInteger prime = reader.get(PRIME_FACTOR); + BigInteger primeExponent = reader.get(FACTOR_CRT_EXPONENT); + BigInteger crtCoefficient = reader.get(FACTOR_CRT_COEFFICIENT); + + return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java new file mode 100644 index 00000000..62fe8868 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RandomSecretKeyBuilder.java @@ -0,0 +1,36 @@ +/* + * 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.security; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class RandomSecretKeyBuilder extends DefaultSecretKeyBuilder { + + public RandomSecretKeyBuilder(String jcaName, int bitLength) { + super(jcaName, bitLength); + } + + @Override + public SecretKey build() { + byte[] bytes = new byte[this.BIT_LENGTH / Byte.SIZE]; + this.random.nextBytes(bytes); + return new SecretKeySpec(bytes, this.JCA_NAME); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java new file mode 100644 index 00000000..07fffd24 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Randoms.java @@ -0,0 +1,54 @@ +/* + * 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.security; + +import java.security.SecureRandom; + +/** + * @since JJWT_RELEASE_VERSION + */ +public final class Randoms { + + private static final SecureRandom DEFAULT_SECURE_RANDOM; + + static { + DEFAULT_SECURE_RANDOM = new SecureRandom(); + DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]); + } + + private Randoms() { + } + + /** + * Returns JJWT's default SecureRandom number generator - a static singleton which may be cached if desired. + * The RNG is initialized using the JVM default as follows: + * + *
    
    +     * static {
    +     *     DEFAULT_SECURE_RANDOM = new SecureRandom();
    +     *     DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]);
    +     * }
    +     * 
    + * + *

    nextBytes is called to force the RNG to initialize itself if not already initialized. The + * byte array is not used and discarded immediately for garbage collection.

    + * + * @return JJWT's default SecureRandom number generator. + */ + public static SecureRandom secureRandom() { + return DEFAULT_SECURE_RANDOM; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java new file mode 100644 index 00000000..4e55d580 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -0,0 +1,224 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAMultiPrimePrivateCrtKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.KeySpec; +import java.security.spec.RSAMultiPrimePrivateCrtKeySpec; +import java.security.spec.RSAOtherPrimeInfo; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.List; +import java.util.Set; + +class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory { + + //All RSA Private fields _except_ for PRIVATE_EXPONENT. That is always required: + private static final Set> OPTIONAL_PRIVATE_FIELDS = Collections.setOf( + DefaultRsaPrivateJwk.FIRST_PRIME, DefaultRsaPrivateJwk.SECOND_PRIME, + DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, + DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT + ); + + private static final String PUBKEY_ERR_MSG = "JwkContext publicKey must be an " + RSAPublicKey.class.getName() + " instance."; + + RsaPrivateJwkFactory() { + super(DefaultRsaPublicJwk.TYPE_VALUE, RSAPrivateKey.class, DefaultRsaPrivateJwk.FIELDS); + } + + @Override + protected boolean supportsKeyValues(JwkContext ctx) { + return super.supportsKeyValues(ctx) && ctx.containsKey(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId()); + } + + private static BigInteger getPublicExponent(RSAPrivateKey key) { + if (key instanceof RSAPrivateCrtKey) { + return ((RSAPrivateCrtKey) key).getPublicExponent(); + } else if (key instanceof RSAMultiPrimePrivateCrtKey) { + return ((RSAMultiPrimePrivateCrtKey) key).getPublicExponent(); + } + + String msg = "Unable to derive RSAPublicKey from RSAPrivateKey implementation [" + + key.getClass().getName() + "]. Supported keys implement the " + + RSAPrivateCrtKey.class.getName() + " or " + RSAMultiPrimePrivateCrtKey.class.getName() + + " interfaces. If the specified RSAPrivateKey cannot be one of these two, you must explicitly " + + "provide an RSAPublicKey in addition to the RSAPrivateKey, as the " + + "[JWA RFC, Section 6.3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2) " + + "requires public values to be present in private RSA JWKs."; + throw new UnsupportedKeyException(msg); + } + + private RSAPublicKey derivePublic(final JwkContext ctx) { + RSAPrivateKey key = ctx.getKey(); + BigInteger modulus = key.getModulus(); + BigInteger publicExponent = getPublicExponent(key); + final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); + return generateKey(ctx, RSAPublicKey.class, new CheckedFunction() { + @Override + public RSAPublicKey apply(KeyFactory kf) { + try { + return (RSAPublicKey) kf.generatePublic(spec); + } catch (Exception e) { + String msg = "Unable to derive RSAPublicKey from RSAPrivateKey " + ctx + + ". Cause: " + e.getMessage(); + throw new UnsupportedKeyException(msg); + } + } + }); + } + + @Override + protected RsaPrivateJwk createJwkFromKey(JwkContext ctx) { + + RSAPrivateKey key = ctx.getKey(); + RSAPublicKey rsaPublicKey; + + PublicKey publicKey = ctx.getPublicKey(); + if (publicKey != null) { + rsaPublicKey = Assert.isInstanceOf(RSAPublicKey.class, publicKey, PUBKEY_ERR_MSG); + } else { + rsaPublicKey = derivePublic(ctx); + } + + // The [JWA Spec](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.1) + // requires public values to be present in private JWKs, so add them: + + // If a JWK fingerprint has been requested to be the JWK id, ensure we copy over the one computed for the + // public key per https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + boolean copyId = !Strings.hasText(ctx.getId()) && ctx.getIdThumbprintAlgorithm() != null; + + JwkContext pubCtx = RsaPublicJwkFactory.INSTANCE.newContext(ctx, rsaPublicKey); + RsaPublicJwk pubJwk = RsaPublicJwkFactory.INSTANCE.createJwk(pubCtx); + ctx.putAll(pubJwk); // add public values to private key context + if (copyId) { + ctx.setId(pubJwk.getId()); + } + + put(ctx, DefaultRsaPrivateJwk.PRIVATE_EXPONENT, key.getPrivateExponent()); + + if (key instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey ckey = (RSAPrivateCrtKey) key; + //noinspection DuplicatedCode + put(ctx, DefaultRsaPrivateJwk.FIRST_PRIME, ckey.getPrimeP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_PRIME, ckey.getPrimeQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, ckey.getPrimeExponentP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, ckey.getPrimeExponentQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, ckey.getCrtCoefficient()); + } else if (key instanceof RSAMultiPrimePrivateCrtKey) { + RSAMultiPrimePrivateCrtKey ckey = (RSAMultiPrimePrivateCrtKey) key; + //noinspection DuplicatedCode + put(ctx, DefaultRsaPrivateJwk.FIRST_PRIME, ckey.getPrimeP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_PRIME, ckey.getPrimeQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, ckey.getPrimeExponentP()); + put(ctx, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, ckey.getPrimeExponentQ()); + put(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, ckey.getCrtCoefficient()); + List infos = Arrays.asList(ckey.getOtherPrimeInfo()); + if (!Collections.isEmpty(infos)) { + put(ctx, DefaultRsaPrivateJwk.OTHER_PRIMES_INFO, infos); + } + } + + return new DefaultRsaPrivateJwk(ctx, pubJwk); + } + + @Override + protected RsaPrivateJwk createJwkFromValues(JwkContext ctx) { + + final FieldReadable reader = new RequiredFieldReader(ctx); + + final BigInteger privateExponent = reader.get(DefaultRsaPrivateJwk.PRIVATE_EXPONENT); + + //The [JWA Spec, Section 6.3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2) requires + //RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully: + JwkContext pubCtx = new DefaultJwkContext<>(DefaultRsaPublicJwk.FIELDS, ctx); + RsaPublicJwk pubJwk = RsaPublicJwkFactory.INSTANCE.createJwkFromValues(pubCtx); + RSAPublicKey pubKey = pubJwk.toKey(); + final BigInteger modulus = pubKey.getModulus(); + final BigInteger publicExponent = pubKey.getPublicExponent(); + + // JWA Section 6.3.2 also indicates that if any of the optional private names are present, then *all* of those + // optional values must be present (except 'oth', which is handled separately next). Quote: + // + // If the producer includes any of the other private key parameters, then all of the others MUST + // be present, with the exception of "oth", which MUST only be present when more than two prime + // factors were used + // + boolean containsOptional = false; + for (Field field : OPTIONAL_PRIVATE_FIELDS) { + if (ctx.containsKey(field.getId())) { + containsOptional = true; + break; + } + } + + KeySpec spec; + + if (containsOptional) { //if any one optional field exists, they are all required per JWA Section 6.3.2: + BigInteger firstPrime = reader.get(DefaultRsaPrivateJwk.FIRST_PRIME); + BigInteger secondPrime = reader.get(DefaultRsaPrivateJwk.SECOND_PRIME); + BigInteger firstCrtExponent = reader.get(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT); + BigInteger secondCrtExponent = reader.get(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT); + BigInteger firstCrtCoefficient = reader.get(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT); + + // Other Primes Info is actually optional even if the above ones are required: + if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId())) { + List otherPrimes = reader.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO); + RSAOtherPrimeInfo[] arr = new RSAOtherPrimeInfo[Collections.size(otherPrimes)]; + otherPrimes.toArray(arr); + spec = new RSAMultiPrimePrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, + secondPrime, firstCrtExponent, secondCrtExponent, firstCrtCoefficient, arr); + } else { + spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, firstPrime, secondPrime, + firstCrtExponent, secondCrtExponent, firstCrtCoefficient); + } + } else { + spec = new RSAPrivateKeySpec(modulus, privateExponent); + } + + RSAPrivateKey key = generateFromSpec(ctx, spec); + ctx.setKey(key); + + return new DefaultRsaPrivateJwk(ctx, pubJwk); + } + + protected RSAPrivateKey generateFromSpec(JwkContext ctx, final KeySpec keySpec) { + return generateKey(ctx, new CheckedFunction() { + @Override + public RSAPrivateKey apply(KeyFactory kf) throws Exception { + return (RSAPrivateKey) kf.generatePrivate(keySpec); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java new file mode 100644 index 00000000..99b34e0f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPublicJwkFactory.java @@ -0,0 +1,62 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.security.RsaPublicJwk; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; + +class RsaPublicJwkFactory extends AbstractFamilyJwkFactory { + + static final RsaPublicJwkFactory INSTANCE = new RsaPublicJwkFactory(); + + RsaPublicJwkFactory() { + super(DefaultRsaPublicJwk.TYPE_VALUE, RSAPublicKey.class, DefaultRsaPublicJwk.FIELDS); + } + + @Override + protected RsaPublicJwk createJwkFromKey(JwkContext ctx) { + RSAPublicKey key = ctx.getKey(); + ctx.put(DefaultRsaPublicJwk.MODULUS.getId(), DefaultRsaPublicJwk.MODULUS.applyTo(key.getModulus())); + ctx.put(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId(), DefaultRsaPublicJwk.PUBLIC_EXPONENT.applyTo(key.getPublicExponent())); + return new DefaultRsaPublicJwk(ctx); + } + + @Override + protected RsaPublicJwk createJwkFromValues(JwkContext ctx) { + FieldReadable reader = new RequiredFieldReader(ctx); + BigInteger modulus = reader.get(DefaultRsaPublicJwk.MODULUS); + BigInteger publicExponent = reader.get(DefaultRsaPublicJwk.PUBLIC_EXPONENT); + final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); + + RSAPublicKey key = generateKey(ctx, new CheckedFunction() { + @Override + public RSAPublicKey apply(KeyFactory keyFactory) throws Exception { + return (RSAPublicKey) keyFactory.generatePublic(spec); + } + }); + + ctx.setKey(key); + + return new DefaultRsaPublicJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java new file mode 100644 index 00000000..c7dc1fc7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2019 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.security; + +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.KeyPairBuilder; +import io.jsonwebtoken.security.SecureRequest; +import io.jsonwebtoken.security.VerifySecureDigestRequest; +import io.jsonwebtoken.security.WeakKeyException; + +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.RSAKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +public class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm { + + private static final String PSS_JCA_NAME = "RSASSA-PSS"; + private static final int MIN_KEY_BIT_LENGTH = 2048; + + private static AlgorithmParameterSpec pssParamFromSaltBitLength(int saltBitLength) { + MGF1ParameterSpec ps = new MGF1ParameterSpec("SHA-" + saltBitLength); + int saltByteLength = saltBitLength / Byte.SIZE; + return new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, saltByteLength, 1); + } + + private final int preferredKeyBitLength; + + private final AlgorithmParameterSpec algorithmParameterSpec; + + public RsaSignatureAlgorithm(String name, String jcaName, int preferredKeyBitLength, AlgorithmParameterSpec algParam) { + super(name, jcaName); + if (preferredKeyBitLength < MIN_KEY_BIT_LENGTH) { + String msg = "preferredKeyBitLength must be greater than the JWA mandatory minimum key length of " + + MIN_KEY_BIT_LENGTH; + throw new IllegalArgumentException(msg); + } + this.preferredKeyBitLength = preferredKeyBitLength; + this.algorithmParameterSpec = algParam; + } + + public RsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength) { + this("RS" + digestBitLength, "SHA" + digestBitLength + "withRSA", preferredKeyBitLength, null); + } + + public RsaSignatureAlgorithm(int digestBitLength, int preferredKeyBitLength, int pssSaltBitLength) { + this("PS" + digestBitLength, PSS_JCA_NAME, preferredKeyBitLength, pssParamFromSaltBitLength(pssSaltBitLength)); + // PSS is not available natively until JDK 11, so try to load BC as a backup provider if possible on <= JDK 10: + setProvider(Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + public Signature get() throws Exception { + return Signature.getInstance(PSS_JCA_NAME); + } + }))); + } + + @Override + public KeyPairBuilder keyPairBuilder() { + return new DefaultKeyPairBuilder("RSA", this.preferredKeyBitLength) + .setProvider(getProvider()) + .setRandom(Randoms.secureRandom()); + } + + @Override + protected void validateKey(Key key, boolean signing) { + + // https://github.com/jwtk/jjwt/issues/68 + if (signing && !(key instanceof PrivateKey)) { + String msg = "Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: " + + key.getClass().getName(); + throw new InvalidKeyException(msg); + } + + // Some PKCS11 providers and HSMs won't expose the RSAKey interface, so we have to check to see if we can cast + // If so, we can provide additional safety checks: + if (key instanceof RSAKey) { + RSAKey rsaKey = (RSAKey) key; + int size = rsaKey.getModulus().bitLength(); + if (size < MIN_KEY_BIT_LENGTH) { + String id = getId(); + String section = id.startsWith("PS") ? "3.5" : "3.3"; + String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + + "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + + section + ") states that RSA keys MUST have a size >= " + MIN_KEY_BIT_LENGTH + " bits. " + + "Consider using the Jwts.SIG." + id + ".generateKeyPair() " + "method to create a " + + "key pair guaranteed to be secure enough for " + id + ". See " + + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + throw new WeakKeyException(msg); + } + } + } + + @Override + protected byte[] doDigest(final SecureRequest request) { + return jca(request).withSignature(new CheckedFunction() { + @Override + public byte[] apply(Signature sig) throws Exception { + if (algorithmParameterSpec != null) { + sig.setParameter(algorithmParameterSpec); + } + sig.initSign(request.getKey()); + sig.update(request.getPayload()); + return sig.sign(); + } + }); + } + + @Override + protected boolean doVerify(final VerifySecureDigestRequest request) { + final Key key = request.getKey(); + if (key instanceof PrivateKey) { //legacy support only TODO: remove for 1.0 + return super.messageDigest(request); + } + return jca(request).withSignature(new CheckedFunction() { + @Override + public Boolean apply(Signature sig) throws Exception { + if (algorithmParameterSpec != null) { + sig.setParameter(algorithmParameterSpec); + } + sig.initVerify(request.getKey()); + sig.update(request.getPayload()); + return sig.verify(request.getDigest()); + } + }); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java new file mode 100644 index 00000000..d8c7f8b4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -0,0 +1,129 @@ +/* + * 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.security; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.FieldReadable; +import io.jsonwebtoken.impl.lang.RequiredFieldReader; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.MacAlgorithm; +import io.jsonwebtoken.security.MalformedKeyException; +import io.jsonwebtoken.security.SecretJwk; +import io.jsonwebtoken.security.SecureDigestAlgorithm; +import io.jsonwebtoken.security.UnsupportedKeyException; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * @since JJWT_RELEASE_VERSION + */ +class SecretJwkFactory extends AbstractFamilyJwkFactory { + + private static final String ENCODED_UNAVAILABLE_MSG = "SecretKey argument does not have any encoded bytes, or " + + "the key's backing JCA Provider is preventing key.getEncoded() from returning any bytes. It is not " + + "possible to represent the SecretKey instance as a JWK."; + + SecretJwkFactory() { + super(DefaultSecretJwk.TYPE_VALUE, SecretKey.class, DefaultSecretJwk.FIELDS); + } + + static byte[] getRequiredEncoded(SecretKey key) { + Assert.notNull(key, "SecretKey argument cannot be null."); + byte[] encoded = null; + Exception cause = null; + try { + encoded = key.getEncoded(); + } catch (Exception e) { + cause = e; + } + + if (Arrays.length(encoded) == 0) { + throw new IllegalArgumentException(ENCODED_UNAVAILABLE_MSG, cause); + } + + return encoded; + } + + @Override + protected SecretJwk createJwkFromKey(JwkContext ctx) { + SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null."); + String k; + try { + byte[] encoded = getRequiredEncoded(key); + k = Encoders.BASE64URL.encode(encoded); + Assert.hasText(k, "k value cannot be null or empty."); + } catch (Exception e) { + String msg = "Unable to encode SecretKey to JWK: " + e.getMessage(); + throw new UnsupportedKeyException(msg, e); + } + + ctx.put(DefaultSecretJwk.K.getId(), k); + + return new DefaultSecretJwk(ctx); + } + + private static void assertKeyBitLength(byte[] bytes, MacAlgorithm alg) { + long bitLen = Bytes.bitLength(bytes); + long requiredBitLen = alg.getKeyBitLength(); + if (bitLen != requiredBitLen) { + // Implementors note: Don't print out any information about the `bytes` value itself - size, + // content, etc., as it is considered secret material: + String msg = "Secret JWK " + AbstractJwk.ALG + " value is '" + alg.getId() + "', but the " + DefaultSecretJwk.K + " length does not equal the '" + alg.getId() + "' length requirement of " + Bytes.bitsMsg(requiredBitLen) + ". This discrepancy could be the result of an algorithm " + "substitution attack or simply an erroneously constructed JWK. In either case, it is likely " + "to result in unexpected or undesired security consequences."; + throw new MalformedKeyException(msg); + } + } + + @Override + protected SecretJwk createJwkFromValues(JwkContext ctx) { + FieldReadable reader = new RequiredFieldReader(ctx); + byte[] bytes = reader.get(DefaultSecretJwk.K); + String jcaName = null; + + String id = ctx.getAlgorithm(); + if (Strings.hasText(id)) { + SecureDigestAlgorithm alg = Jwts.SIG.find(id); + if (alg instanceof MacAlgorithm) { + jcaName = ((CryptoAlgorithm) alg).getJcaName(); // valid for all JJWT alg implementations + Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty."); + assertKeyBitLength(bytes, (MacAlgorithm) alg); + } + } + if (!Strings.hasText(jcaName) && + ("sig".equalsIgnoreCase(ctx.getPublicKeyUse()) || // [1] + (!Collections.isEmpty(ctx.getOperations()) && ctx.getOperations().contains("sign")))) { // [2] + // [1] Even though 'use' is for PUBLIC KEY use (as defined in RFC 7515), RFC 7520 shows secret keys with + // 'use' values, so we'll account for that as well + // + // [2] operations values are case-sensitive, so we don't need to test for upper/lowercase "sign" values + + // The only JWA SecretKey signature algorithms are HS256, HS384, HS512, so choose based on bit length: + jcaName = "HmacSHA" + Bytes.bitLength(bytes); + } + if (jcaName == null) { // not an HS* algorithm, no signature "use", no "sign" key op, so default to encryption: + jcaName = AesAlgorithm.KEY_ALG_NAME; + } + + SecretKey key = new SecretKeySpec(bytes, jcaName); + ctx.setKey(key); + return new DefaultSecretJwk(ctx); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithmsBridge.java new file mode 100644 index 00000000..41d538bd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardEncryptionAlgorithmsBridge.java @@ -0,0 +1,33 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.IdRegistry; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.AeadAlgorithm; + +@SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.security.StandardEncryptionAlgorithms +public final class StandardEncryptionAlgorithmsBridge extends DelegatingRegistry { + public StandardEncryptionAlgorithmsBridge() { + super(new IdRegistry<>("JWE Encryption Algorithm", Collections.of( + (AeadAlgorithm) new HmacAesAeadAlgorithm(128), + new HmacAesAeadAlgorithm(192), + new HmacAesAeadAlgorithm(256), + new GcmAesAeadAlgorithm(128), + new GcmAesAeadAlgorithm(192), + new GcmAesAeadAlgorithm(256)))); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithmsBridge.java new file mode 100644 index 00000000..9a99d075 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardHashAlgorithmsBridge.java @@ -0,0 +1,70 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.CheckedSupplier; +import io.jsonwebtoken.impl.lang.Conditions; +import io.jsonwebtoken.impl.lang.IdRegistry; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.HashAlgorithm; + +import java.security.MessageDigest; +import java.security.Provider; +import java.util.Locale; + +/** + * Backing implementation for the {@link io.jsonwebtoken.security.StandardHashAlgorithms} implementation. + * + * @since JJWT_RELEASE_VERSION + */ +@SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.security.StandardHashAlgorithms +public class StandardHashAlgorithmsBridge extends DelegatingRegistry { + + public StandardHashAlgorithmsBridge() { + super(new IdRegistry<>("IANA Hash Algorithm", Collections.of( + // We don't include DefaultHashAlgorithm.SHA1 here on purpose because 1) it's not in the JWK IANA + // registry so we don't need to expose it anyway, and 2) we don't want to expose a less-safe algorithm. + // The SHA1 instance only exists in JJWT's codebase to support RFC-required `x5t` + // (X.509 SHA-1 Thumbprint) computation - we don't use it anywhere else. + (HashAlgorithm) new DefaultHashAlgorithm("sha-256"), + new DefaultHashAlgorithm("sha-384"), + new DefaultHashAlgorithm("sha-512"), + fallbackProvider("sha3-256"), + fallbackProvider("sha3-384"), + fallbackProvider("sha3-512") + ))); + } + + private static DefaultHashAlgorithm fallbackProvider(String id) { + String jcaName = id.toUpperCase(Locale.ENGLISH); + Provider provider = Providers.findBouncyCastle(Conditions.notExists(new MessageDigestSupplier(jcaName))); + return new DefaultHashAlgorithm(id, jcaName, provider); + } + + private static class MessageDigestSupplier implements CheckedSupplier { + private final String jcaName; + + private MessageDigestSupplier(String jcaName) { + this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); + } + + @Override + public MessageDigest get() throws Exception { + return MessageDigest.getInstance(jcaName); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardKeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardKeyAlgorithmsBridge.java new file mode 100644 index 00000000..251c696e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardKeyAlgorithmsBridge.java @@ -0,0 +1,228 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.IdRegistry; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.KeyAlgorithm; + +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; + +/** + * Static class definitions for standard {@link KeyAlgorithm} instances. + * + * @since JJWT_RELEASE_VERSION + */ +public final class StandardKeyAlgorithmsBridge extends DelegatingRegistry> { + + private static final String RSA1_5_ID = "RSA1_5"; + private static final String RSA1_5_TRANSFORMATION = "RSA/ECB/PKCS1Padding"; + private static final String RSA_OAEP_ID = "RSA-OAEP"; + private static final String RSA_OAEP_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + private static final String RSA_OAEP_256_ID = "RSA-OAEP-256"; + private static final String RSA_OAEP_256_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + private static final AlgorithmParameterSpec RSA_OAEP_256_SPEC = + new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); + + public StandardKeyAlgorithmsBridge() { + super(new IdRegistry<>("JWE Key Management Algorithm", Collections.>of( + new DirectKeyAlgorithm(), + new AesWrapKeyAlgorithm(128), + new AesWrapKeyAlgorithm(192), + new AesWrapKeyAlgorithm(256), + new AesGcmKeyAlgorithm(128), + new AesGcmKeyAlgorithm(192), + new AesGcmKeyAlgorithm(256), + new Pbes2HsAkwAlgorithm(128), + new Pbes2HsAkwAlgorithm(192), + new Pbes2HsAkwAlgorithm(256), + new EcdhKeyAlgorithm(), + new EcdhKeyAlgorithm(new AesWrapKeyAlgorithm(128)), + new EcdhKeyAlgorithm(new AesWrapKeyAlgorithm(192)), + new EcdhKeyAlgorithm(new AesWrapKeyAlgorithm(256)), + new DefaultRsaKeyAlgorithm(RSA1_5_ID, RSA1_5_TRANSFORMATION), + new DefaultRsaKeyAlgorithm(RSA_OAEP_ID, RSA_OAEP_TRANSFORMATION), + new DefaultRsaKeyAlgorithm(RSA_OAEP_256_ID, RSA_OAEP_256_TRANSFORMATION, RSA_OAEP_256_SPEC) + ))); + } + + /* + private static KeyAlgorithm lean(final Pbes2HsAkwAlgorithm alg) { + + // ensure we use the same key factory over and over so that time spent acquiring one is not repeated: + JcaTemplate template = new JcaTemplate(alg.getJcaName(), null, Randoms.secureRandom()); + final SecretKeyFactory factory = template.execute(SecretKeyFactory.class, new CheckedFunction() { + @Override + public SecretKeyFactory apply(SecretKeyFactory secretKeyFactory) { + return secretKeyFactory; + } + }); + + // pre-compute the salt so we don't spend time doing that on each iteration. Doesn't need to be random for a + // computation-only test: + final byte[] rfcSalt = alg.toRfcSalt(alg.generateInputSalt(null)); + + // ensure that the bare minimum steps are performed to hash, ensuring our time sampling pertains only to + // hashing and not ancillary steps needed to setup the hashing/derivation + return new KeyAlgorithm() { + @Override + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + int iterations = request.getHeader().getPbes2Count(); + char[] password = request.getKey().getPassword(); + try { + alg.deriveKey(factory, password, rfcSalt, iterations); + } catch (Exception e) { + throw new SecurityException("Unable to derive key", e); + } + return null; + } + + @Override + public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + throw new UnsupportedOperationException("Not intended to be called."); + } + + @Override + public String getId() { + return alg.getId(); + } + }; + } + + private static char randomChar() { + return (char) Randoms.secureRandom().nextInt(Character.MAX_VALUE); + } + + private static char[] randomChars(@SuppressWarnings("SameParameterValue") int length) { + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = randomChar(); + } + return chars; + } + + public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { + + // The number of computational samples that land in our 'sweet spot' timing range matching desiredMillis. + // These samples will be averaged and the final average will be the return value of this method + // representing the number of iterations that should be taken for any given PBE hashing attempt to get + // reasonably close to desiredMillis: + final int NUM_SAMPLES = 30; + final int SKIP = 3; + // More important than the actual password (or characters) is the password length. + // 8 characters is a commonly-found minimum required length in many systems circa 2021. + final int PASSWORD_LENGTH = 8; + + final JweHeader HEADER = new DefaultJweHeader(); + final AeadAlgorithm ENC_ALG = Jwts.ENC.A128GCM; // not used, needed to satisfy API + + if (alg instanceof Pbes2HsAkwAlgorithm) { + // Strip away all things that cause time during computation except for the actual hashing algorithm: + alg = lean((Pbes2HsAkwAlgorithm) alg); + } + + int workFactor = 1000; // same as iterations for PBKDF2. Different concept for Bcrypt/Scrypt + int minWorkFactor = workFactor; + List points = new ArrayList<>(NUM_SAMPLES); + for (int i = 0; points.size() < NUM_SAMPLES; i++) { + + char[] password = randomChars(PASSWORD_LENGTH); + Password key = Keys.forPassword(password); + HEADER.setPbes2Count(workFactor); + KeyRequest request = new DefaultKeyRequest<>(null, null, key, HEADER, ENC_ALG); + + long start = System.currentTimeMillis(); + alg.getEncryptionKey(request); // <-- Computation occurs here. Don't need the result, just need to exec + long end = System.currentTimeMillis(); + long duration = end - start; + + // Exclude the first SKIP number of attempts from the average due to initial JIT optimization/slowness. + // After a few attempts, the JVM should be relatively optimized and the subsequent + // PBE hashing times are the ones we want to include in our analysis + boolean warmedUp = i >= SKIP; + + // how close we were on this hashing attempt to reach our desiredMillis target: + // A number under 1 means we weren't slow enough, a number greater than 1 means we were too slow: + double durationPercentAchieved = (double) duration / (double) desiredMillis; + + // we only want to collect timing samples if : + // 1. we're warmed up (to account for JIT optimization) + // 2. The attempt time at least met (>=) the desiredMillis target + boolean collectSample = warmedUp && duration >= desiredMillis; + if (collectSample) { + // For each attempt, the x axis is the workFactor, and the y axis is how long it took to compute: + points.add(new Point(workFactor, duration)); + //System.out.println("Collected point: workFactor=" + workFactor + ", duration=" + duration + " ms, %achieved=" + durationPercentAchieved); + } else { + minWorkFactor = Math.max(minWorkFactor, workFactor); + //System.out.println(" Excluding sample: workFactor=" + workFactor + ", duration=" + duration + " ms, %achieved=" + durationPercentAchieved); + } + + // amount to increase or decrease the workFactor for the next hashing iteration. We increase if + // we haven't met the desired millisecond time, and decrease if we're over it a little too much, always + // trying to stay in that desired timing sweet spot + double percentAdjust = workFactor * 0.0075; // 3/4ths of a percent + if (durationPercentAchieved < 1d) { + // Under target. Let's increase by the amount that should get right at (or near) 100%: + double ratio = desiredMillis / (double) duration; + if (ratio > 1) { + double result = workFactor * ratio; + workFactor = (int) result; + } else { + double difference = workFactor * (1 - durationPercentAchieved); + workFactor += Math.max(percentAdjust, difference); + } + } else if (durationPercentAchieved > 1.01d) { + // Over target. Let's decrease gently to get closer. + double difference = workFactor * (durationPercentAchieved - 1.01); + difference = Math.min(percentAdjust, difference); + // math.max here because the min allowed is 1000 per the JWA RFC, so we never want to go below that. + workFactor = (int) Math.max(1000, workFactor - difference); + } else { + // we're at our target (desiredMillis); let's increase by a teeny bit to see where we get + // (and the JVM might optimize with the same inputs, so we want to prevent that here) + workFactor += 100; + } + } + + // We've collected all of our samples, now let's find the workFactor average number + // That average is the best estimate for ensuring PBE hashes for the specified algorithm meet the + // desiredMillis target on the current JVM/CPU platform: + double sumX = 0; + for (Point p : points) { + sumX += p.x; + } + double average = sumX / points.size(); + //ensure our average is at least as much as the smallest work factor that got us closest to desiredMillis: + return (int) Math.max(average, minWorkFactor); + } + + private static class Point { + long x; + long y; + double lnY; + + public Point(long x, long y) { + this.x = x; + this.y = y; + this.lnY = Math.log((double) y); + } + } + */ +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/StandardSecureDigestAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardSecureDigestAlgorithmsBridge.java new file mode 100644 index 00000000..7a74fc9f --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/StandardSecureDigestAlgorithmsBridge.java @@ -0,0 +1,66 @@ +/* + * 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.security; + +import io.jsonwebtoken.impl.lang.IdRegistry; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.SecureDigestAlgorithm; + +@SuppressWarnings("unused") // used via reflection in io.jsonwebtoken.security.StandardSecureDigestAlgorithms +public final class StandardSecureDigestAlgorithmsBridge extends DelegatingRegistry> { + + public StandardSecureDigestAlgorithmsBridge() { + super(new IdRegistry<>("JWS Digital Signature or MAC", Collections.of( + new NoneSignatureAlgorithm(), + new DefaultMacAlgorithm(256), + new DefaultMacAlgorithm(384), + new DefaultMacAlgorithm(512), + new RsaSignatureAlgorithm(256, 2048), + new RsaSignatureAlgorithm(384, 3072), + new RsaSignatureAlgorithm(512, 4096), + new RsaSignatureAlgorithm(256, 2048, 256), + new RsaSignatureAlgorithm(384, 3072, 384), + new RsaSignatureAlgorithm(512, 4096, 512), + new EcSignatureAlgorithm(256), + new EcSignatureAlgorithm(384), + new EcSignatureAlgorithm(521), + new EdSignatureAlgorithm() + ))); + } + + private static final EdSignatureAlgorithm Ed25519 = new EdSignatureAlgorithm(EdwardsCurve.Ed25519); + private static final EdSignatureAlgorithm Ed448 = new EdSignatureAlgorithm(EdwardsCurve.Ed448); + + @Override + public SecureDigestAlgorithm find(String id) { + if (EdwardsCurve.Ed448.getId().equalsIgnoreCase(id)) { + return Ed448; + } else if (EdwardsCurve.Ed25519.getId().equalsIgnoreCase(id)) { + return Ed25519; + } + return super.find(id); + } + + @Override + public SecureDigestAlgorithm get(String id) throws IllegalArgumentException { + if (EdwardsCurve.Ed448.getId().equalsIgnoreCase(id)) { + return Ed448; + } else if (EdwardsCurve.Ed25519.getId().equalsIgnoreCase(id)) { + return Ed25519; + } + return super.get(id); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy similarity index 53% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java rename to impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy index 2e542836..b2ff6703 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ b/impl/src/test/groovy/io/jsonwebtoken/CompressionCodecsTest.groovy @@ -13,24 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.compression.DeflateCompressionCodec +import io.jsonwebtoken.impl.compression.GzipCompressionCodec +import org.junit.Test -import java.security.Key; -import java.security.MessageDigest; +import static org.junit.Assert.assertTrue -public class MacValidator implements SignatureValidator { +class CompressionCodecsTest { - private final MacSigner signer; - - public MacValidator(SignatureAlgorithm alg, Key key) { - this.signer = new MacSigner(alg, key); + @Test + void testCtor() { + //test coverage for private constructor: + new CompressionCodecs() } - @Override - public boolean isValid(byte[] data, byte[] signature) { - byte[] computed = this.signer.sign(data); - return MessageDigest.isEqual(computed, signature); + @Test + void testStatics() { + assertTrue CompressionCodecs.DEFLATE instanceof DeflateCompressionCodec + assertTrue CompressionCodecs.GZIP instanceof GzipCompressionCodec } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy index 5652bb00..62069823 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/CustomObjectDeserializationTest.groovy @@ -19,10 +19,8 @@ import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.jackson.io.JacksonDeserializer import org.junit.Test -import static org.hamcrest.CoreMatchers.is import static org.junit.Assert.assertEquals import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertThat class CustomObjectDeserializationTest { @@ -39,16 +37,16 @@ class CustomObjectDeserializationTest { String jwtString = Jwts.builder().claim("cust", customBean).compact() // no custom deserialization, object is a map - Jwt jwt = Jwts.parser().parseClaimsJwt(jwtString) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(jwtString) assertNotNull jwt - assertEquals jwt.getBody().get('cust'), [key1: 'value1', key2: 42] + assertEquals jwt.getPayload().get('cust'), [key1: 'value1', key2: 42] // custom type for 'cust' claim Deserializer deserializer = new JacksonDeserializer([cust: CustomBean]) - jwt = Jwts.parser().deserializeJsonWith(deserializer).parseClaimsJwt(jwtString) + jwt = Jwts.parserBuilder().enableUnsecuredJws().deserializeJsonWith(deserializer).build().parseClaimsJwt(jwtString) assertNotNull jwt - CustomBean result = jwt.getBody().get("cust", CustomBean) - assertThat result, is(customBean) + CustomBean result = jwt.getPayload().get("cust", CustomBean) + assertEquals customBean, result } static class CustomBean { diff --git a/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy b/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy index 9eee6e74..e8c99675 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2019 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 final class DateTestUtils { diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index fa8f717e..0b5fbea7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -17,16 +17,18 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultClock import io.jsonwebtoken.impl.FixedClock +import io.jsonwebtoken.impl.JwtTokenizer +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.SignatureException import org.junit.Test -import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets import java.security.SecureRandom -import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE -import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static org.junit.Assert.* class DeprecatedJwtParserTest { @@ -45,23 +47,6 @@ class DeprecatedJwtParserTest { return Encoders.BASE64URL.encode(bytes) } - @Test - void testSetDuplicateSigningKeys() { - - byte[] keyBytes = randomKey() - - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256") - - String compact = Jwts.builder().setPayload('Hello World!').signWith(SignatureAlgorithm.HS256, keyBytes).compact() - - try { - Jwts.parser().setSigningKey(keyBytes).setSigningKey(key).parse(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A key object and key bytes cannot both be specified. Choose either.' - } - } - @Test void testIsSignedWithNullArgument() { assertFalse Jwts.parser().isSigned(null) @@ -80,10 +65,10 @@ class DeprecatedJwtParserTest { String bad = base64Url('{"alg":"none"}') + '.' + base64Url(junkPayload) + '.' try { - Jwts.parser().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload + assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() } } @@ -129,7 +114,7 @@ class DeprecatedJwtParserTest { } @Test - void testParsePlaintextJwsWithIncorrectAlg() { + void testParseContentJwsWithIncorrectAlg() { def header = '{"alg":"none"}' @@ -140,10 +125,10 @@ class DeprecatedJwtParserTest { String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { - Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals se.getMessage(), 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() } } @@ -162,9 +147,9 @@ class DeprecatedJwtParserTest { assertTrue Jwts.parser().isSigned(compact) - Jwt jwt = Jwts.parser().setSigningKey(base64Encodedkey).parse(compact) + def jwt = Jwts.parser().setSigningKey(base64Encodedkey).parse(compact) - assertEquals jwt.body, payload + assertEquals payload, new String(jwt.body as byte[], StandardCharsets.UTF_8) } @Test @@ -175,7 +160,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -193,7 +178,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parser().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -210,7 +195,7 @@ class DeprecatedJwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() - Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -222,7 +207,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -236,7 +221,7 @@ class DeprecatedJwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() - Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -248,7 +233,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() try { - Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -256,59 +241,59 @@ class DeprecatedJwtParserTest { } // ======================================================================== - // parsePlaintextJwt tests + // parseContentJwt tests // ======================================================================== @Test - void testParsePlaintextJwt() { + void testParseContentJwt() { String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).compact() - Jwt jwt = Jwts.parser().parsePlaintextJwt(compact) + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) - assertEquals jwt.getBody(), payload + assertEquals payload, new String(jwt.body, StandardCharsets.UTF_8) } @Test - void testParsePlaintextJwtWithClaimsJwt() { + void testParseContentJwtWithClaimsJwt() { String compact = Jwts.builder().setSubject('Joe').compact() try { - Jwts.parser().parsePlaintextJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals e.getMessage(), 'Unprotected Claims JWTs are not supported.' } } @Test - void testParsePlaintextJwtWithPlaintextJws() { + void testParseContentJwtWithContentJws() { String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parsePlaintextJws(compact) + Jwts.parser().parseContentJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @Test - void testParsePlaintextJwtWithClaimsJws() { + void testParseContentJwtWithClaimsJws() { String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parsePlaintextJws(compact) + Jwts.parser().parseContentJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -323,28 +308,28 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() - Jwt jwt = Jwts.parser().parseClaimsJwt(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) assertEquals jwt.getBody().getSubject(), subject } @Test - void testParseClaimsJwtWithPlaintextJwt() { + void testParseClaimsJwtWithContentJwt() { String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @Test - void testParseClaimsJwtWithPlaintextJws() { + void testParseClaimsJwtWithContentJws() { String payload = 'Hello world!' @@ -354,7 +339,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -367,7 +352,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @@ -381,7 +366,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -396,7 +381,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parser().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -404,11 +389,11 @@ class DeprecatedJwtParserTest { } // ======================================================================== - // parsePlaintextJws tests + // parseContentJws tests // ======================================================================== @Test - void testParsePlaintextJws() { + void testParseContentJws() { String payload = 'Hello world!' @@ -416,13 +401,13 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key).compact() - Jwt jwt = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + def jwt = Jwts.parser().setSigningKey(key).parseContentJws(compact) - assertEquals jwt.getBody(), payload + assertEquals payload, new String(jwt.body, StandardCharsets.UTF_8) } @Test - void testParsePlaintextJwsWithPlaintextJwt() { + void testParseContentJwsWithContentJwt() { String payload = 'Hello world!' @@ -431,15 +416,15 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @Test - void testParsePlaintextJwsWithClaimsJwt() { + void testParseContentJwsWithClaimsJwt() { String subject = 'Joe' @@ -448,15 +433,15 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', e.getMessage() } } @Test - void testParsePlaintextJwsWithClaimsJws() { + void testParseContentJwsWithClaimsJws() { String subject = 'Joe' @@ -465,10 +450,10 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parser().setSigningKey(key).parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -535,7 +520,7 @@ class DeprecatedJwtParserTest { } @Test - void testParseClaimsJwsWithPlaintextJwt() { + void testParseClaimsJwsWithContentJwt() { String payload = 'Hello world!' @@ -544,10 +529,10 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @@ -561,27 +546,27 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', e.getMessage() } } @Test - void testParseClaimsJwsWithPlaintextJws() { + void testParseClaimsJwsWithContentJws() { - String subject = 'Joe' + String payload = 'Hello world' byte[] key = randomKey() - String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() + String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed content JWTs are not supported.', e.getMessage() } } @@ -634,54 +619,6 @@ class DeprecatedJwtParserTest { } } - @Test - void testParseClaimsWithSigningKeyResolverAndKey() { - - String subject = 'Joe' - - SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") - - String compact = Jwts.builder().setSubject(subject).signWith(key, SignatureAlgorithm.HS256).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' - } - } - - @Test - void testParseClaimsWithSigningKeyResolverAndKeyBytes() { - - String subject = 'Joe' - - byte[] key = randomKey() - - String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' - } - } - @Test void testParseClaimsWithNullSigningKeyResolver() { @@ -751,11 +688,11 @@ class DeprecatedJwtParserTest { } // ======================================================================== - // parsePlaintextJws with signingKey resolver. + // parseContentJws with signingKey resolver. // ======================================================================== @Test - void testParsePlaintextJwsWithSigningKeyResolverAdapter() { + void testParseContentJwsWithSigningKeyResolverAdapter() { String inputPayload = 'Hello world!' @@ -765,34 +702,34 @@ class DeprecatedJwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() { @Override - byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + byte[] resolveSigningKeyBytes(JwsHeader header, byte[] payload) { return key } } - Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseContentJws(compact) - assertEquals jws.getBody(), inputPayload + assertEquals inputPayload, new String(jws.body, StandardCharsets.UTF_8) } @Test - void testParsePlaintextJwsWithSigningKeyResolverInvalidKey() { + void testParseContentJwsWithSigningKeyResolverInvalidKey() { - String inputPayload = 'Hello world!' + byte[] inputPayload = 'Hello world!'.getBytes(StandardCharsets.UTF_8) byte[] key = randomKey() - String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() + String compact = Jwts.builder().setContent(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact() def signingKeyResolver = new SigningKeyResolverAdapter() { @Override - byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + byte[] resolveSigningKeyBytes(JwsHeader header, byte[] payload) { return randomKey() } } try { - Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseContentJws(compact) fail() } catch (SignatureException se) { assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' @@ -800,7 +737,7 @@ class DeprecatedJwtParserTest { } @Test - void testParsePlaintextJwsWithInvalidSigningKeyResolverAdapter() { + void testParseContentJwsWithInvalidSigningKeyResolverAdapter() { String payload = 'Hello world!' @@ -811,12 +748,12 @@ class DeprecatedJwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() try { - Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseContentJws(compact) fail() } catch (UnsupportedJwtException ex) { - assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + - 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, String) ' + - 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support content ' + + 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, byte[]) ' + + 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, byte[]) method.' } } @@ -1459,7 +1396,7 @@ class DeprecatedJwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() - Jwts.parser().setClock(new FixedClock(beforeExpiry)).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new FixedClock(beforeExpiry)).build().parse(compact) } @Test @@ -1472,14 +1409,41 @@ class DeprecatedJwtParserTest { } } + @Test + void testSetClock() { + def clock = new DefaultClock(); + def parser = Jwts.parser().setClock(clock) + assertSame clock, parser.@clock + assertFalse DefaultClock.INSTANCE.is(parser.@clock) + } + @Test void testParseClockManipulationWithDefaultClock() { Date expiry = new Date(System.currentTimeMillis() - 1000) + def key = TestKeys.HS256 + + String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry) + .signWith(key).compact() + + try { + def clock = new DefaultClock() + def parser = Jwts.parser().setSigningKey(key).setClock(clock) + parser.parseClaimsJws(compact) + fail() + } catch (ExpiredJwtException e) { + assertTrue e.getMessage().startsWith('JWT expired at ') + } + } + + @Test + void testBuilderParseClockManipulationWithDefaultClock() { + Date expiry = new Date(System.currentTimeMillis() - 1000) + String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() try { - Jwts.parser().setClock(new DefaultClock()).parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new DefaultClock()).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -1503,37 +1467,23 @@ class DeprecatedJwtParserTest { Jwts.parser().setSigningKey(randomKey()).parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '3' + assertEquals expected, se.message } } @Test - void testNoHeaderNoSig() { + void testNoProtectedHeader() { - String payload = '{"subject":"Joe"}' + String payload = '{"sub":"Joe"}' String jwtStr = '.' + base64Url(payload) + '.' - Jwt jwt = Jwts.parser().parse(jwtStr) - - assertTrue jwt.header == null - assertEquals 'Joe', jwt.body.get('subject') - } - - @Test - void testNoHeaderSig() { - - String payload = '{"subject":"Joe"}' - - String sig = ";aklsjdf;kajsd;fkjas;dklfj" - - String jwtStr = '.' + base64Url(payload) + '.' + base64Url(sig) - try { - Jwts.parser().parse(jwtStr) + Jwts.parserBuilder().build().parse(jwtStr) fail() - } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + } catch (MalformedJwtException e) { + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.getMessage() } } @@ -1549,10 +1499,10 @@ class DeprecatedJwtParserTest { String jwtStr = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(sig) try { - Jwts.parser().parse(jwtStr) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy index 0b0bf55f..827a5909 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtsTest.groovy @@ -15,11 +15,13 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.DefaultUnprotectedHeader +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings @@ -30,12 +32,14 @@ import org.junit.Test import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import java.nio.charset.Charset +import java.nio.charset.StandardCharsets import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import static org.junit.Assert.* +@SuppressWarnings(['GrDeprecatedAPIUsage', 'GrUnnecessarySemicolon']) class DeprecatedJwtsTest { private static Date now() { @@ -78,14 +82,14 @@ class DeprecatedJwtsTest { @Test void testHeaderWithNoArgs() { - def header = Jwts.header() - assertTrue header instanceof DefaultHeader + def header = Jwts.unprotectedHeader() + assertTrue header instanceof DefaultUnprotectedHeader } @Test void testHeaderWithMapArg() { def header = Jwts.header([alg: "HS256"]) - assertTrue header instanceof DefaultHeader + assertTrue header instanceof DefaultUnprotectedHeader assertEquals header.alg, 'HS256' } @@ -116,7 +120,7 @@ class DeprecatedJwtsTest { } @Test - void testPlaintextJwtString() { + void testContentJwtString() { // Assert exact output per example at https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 @@ -124,6 +128,7 @@ class DeprecatedJwtsTest { // carriage return + newline, so we have to include them in the test payload to assert our encoded output // matches what is in the spec: + //noinspection HttpUrlsUsage def payload = '{"iss":"joe",\r\n' + ' "exp":1300819380,\r\n' + ' "http://example.com/is_root":true}' @@ -136,13 +141,13 @@ class DeprecatedJwtsTest { } @Test - void testParsePlaintextToken() { + void testParseContentToken() { def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] String jwt = Jwts.builder().setClaims(claims).compact(); - def token = Jwts.parser().parse(jwt); + def token = Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwt); //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -169,7 +174,8 @@ class DeprecatedJwtsTest { Jwts.parser().parse('foo') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 0" + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '0' + assertEquals expected, e.message } } @@ -179,51 +185,32 @@ class DeprecatedJwtsTest { Jwts.parser().parse('.') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 1" + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '1' + assertEquals expected, e.message } } - @Test + @Test(expected = MalformedJwtException) void testParseWithTwoPeriodsOnly() { - try { - Jwts.parser().parse('..') - fail() - } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string '..' is missing a header." - } + Jwts.parser().parse('..') } @Test void testParseWithHeaderOnly() { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".." - Jwt jwt = Jwts.parser().parse(unsecuredJwt) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(unsecuredJwt) assertEquals("none", jwt.getHeader().get("alg")) } - @Test + @Test(expected = MalformedJwtException) void testParseWithSignatureOnly() { - try { - Jwts.parser().parse('..bar') - fail() - } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm." - } - } - - @Test - void testWithInvalidCompressionAlgorithm() { - try { - - Jwts.builder().setHeaderParam(Header.COMPRESSION_ALGORITHM, "CUSTOM").setId("andId").compact() - } catch (CompressionException e) { - assertEquals "Unsupported compression algorithm 'CUSTOM'", e.getMessage() - } + Jwts.parser().parse('..bar') } @Test void testConvenienceIssuer() { String compact = Jwts.builder().setIssuer("Me").compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getIssuer(), "Me" compact = Jwts.builder().setSubject("Joe") @@ -231,14 +218,14 @@ class DeprecatedJwtsTest { .setIssuer(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuer() } @Test void testConvenienceSubject() { String compact = Jwts.builder().setSubject("Joe").compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getSubject(), "Joe" compact = Jwts.builder().setIssuer("Me") @@ -246,14 +233,14 @@ class DeprecatedJwtsTest { .setSubject(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getSubject() } @Test void testConvenienceAudience() { String compact = Jwts.builder().setAudience("You").compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getAudience(), "You" compact = Jwts.builder().setIssuer("Me") @@ -261,7 +248,7 @@ class DeprecatedJwtsTest { .setAudience(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getAudience() } @@ -269,7 +256,7 @@ class DeprecatedJwtsTest { void testConvenienceExpiration() { Date then = laterDate(10000) String compact = Jwts.builder().setExpiration(then).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getExpiration() assertEquals claimedDate, then @@ -278,7 +265,7 @@ class DeprecatedJwtsTest { .setExpiration(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getExpiration() } @@ -286,7 +273,7 @@ class DeprecatedJwtsTest { void testConvenienceNotBefore() { Date now = now() //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setNotBefore(now).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getNotBefore() assertEquals claimedDate, now @@ -295,7 +282,7 @@ class DeprecatedJwtsTest { .setNotBefore(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getNotBefore() } @@ -303,7 +290,7 @@ class DeprecatedJwtsTest { void testConvenienceIssuedAt() { Date now = now() //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setIssuedAt(now).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims def claimedDate = claims.getIssuedAt() assertEquals claimedDate, now @@ -312,7 +299,7 @@ class DeprecatedJwtsTest { .setIssuedAt(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getIssuedAt() } @@ -320,7 +307,7 @@ class DeprecatedJwtsTest { void testConvenienceId() { String id = UUID.randomUUID().toString(); String compact = Jwts.builder().setId(id).compact(); - Claims claims = Jwts.parser().parse(compact).body as Claims + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertEquals claims.getId(), id compact = Jwts.builder().setIssuer("Me") @@ -328,7 +315,7 @@ class DeprecatedJwtsTest { .setId(null) //null should remove it .compact(); - claims = Jwts.parser().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).body as Claims assertNull claims.getId() } @@ -409,7 +396,7 @@ class DeprecatedJwtsTest { String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -418,6 +405,7 @@ class DeprecatedJwtsTest { @Override CompressionCodec resolveCompressionCodec(Header header) { String algorithm = header.getCompressionAlgorithm() + //noinspection ChangeToOperator if ("CUSTOM".equals(algorithm)) { return CompressionCodecs.GZIP } else { @@ -447,7 +435,7 @@ class DeprecatedJwtsTest { String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -466,13 +454,13 @@ class DeprecatedJwtsTest { String compact = Jwts.builder().setPayload(payload).signWith(alg, key) .compressWith(CompressionCodecs.DEFLATE).compact() - def jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + def jws = Jwts.parser().setSigningKey(key).parseContentJws(compact) - String parsed = jws.body + byte[] parsed = jws.body assertEquals "DEF", jws.header.getCompressionAlgorithm() - assertEquals "this is my test for a payload", parsed + assertEquals "this is my test for a payload", new String(parsed, StandardCharsets.UTF_8) } @Test @@ -552,11 +540,14 @@ class DeprecatedJwtsTest { @Test void testES256WithPrivateKeyValidation() { + def alg = SignatureAlgorithm.ES256; try { - testEC(SignatureAlgorithm.ES256, true) + testEC(alg, true) fail("EC private keys cannot be used to validate EC signatures.") } catch (UnsupportedJwtException e) { - assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey instance." + String msg = "${alg.name()} verification keys must be PublicKeys (implement java.security.PublicKey). " + + "Provided key type: sun.security.ec.ECPrivateKeyImpl." + assertEquals msg, e.cause.message } } @@ -569,6 +560,7 @@ class DeprecatedJwtsTest { String jws = Jwts.builder().setSubject("Foo").signWith(key, alg).compact() + //noinspection GroovyUnusedCatchParameter try { Jwts.parser().setSigningKey(weakKey).parseClaimsJws(jws) fail('parseClaimsJws must fail for weak keys') @@ -587,10 +579,10 @@ class DeprecatedJwtsTest { String notSigned = Jwts.builder().setSubject("Foo").compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(notSigned) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(notSigned) fail('parseClaimsJws must fail for unsigned JWTs') } catch (UnsupportedJwtException expected) { - assertEquals expected.message, 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', expected.message } } @@ -613,17 +605,17 @@ class DeprecatedJwtsTest { String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: - assertEquals Jwts.parser().parseClaimsJwt(forged).getHeader().get('alg'), 'none' + assertEquals 'none', Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(forged).getHeader().get('alg') //now let's forge it by appending the signature the server expects: forged += signature //now assert that, when the server tries to parse the forged token, parsing fails: try { - Jwts.parser().setSigningKey(key).parse(forged) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { - assertEquals expected.message, 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message } } @@ -632,7 +624,7 @@ class DeprecatedJwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -664,7 +656,7 @@ class DeprecatedJwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); @@ -696,7 +688,7 @@ class DeprecatedJwtsTest { void testParseForgedEllipticCurvePublicKeyAsHmacToken() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.ES256) + KeyPair kp = TestKeys.ES256.pair PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); @@ -740,6 +732,7 @@ class DeprecatedJwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) + //noinspection GrEqualsBetweenInconvertibleTypes assert [alg: alg.name()] == token.header //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -756,6 +749,7 @@ class DeprecatedJwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) + //noinspection GrEqualsBetweenInconvertibleTypes assert token.header == [alg: alg.name()] //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims @@ -778,6 +772,7 @@ class DeprecatedJwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) + //noinspection GrEqualsBetweenInconvertibleTypes assert token.header == [alg: alg.name()] //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index e5b46bb6..11ab6fbb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -16,22 +16,27 @@ package io.jsonwebtoken import io.jsonwebtoken.impl.DefaultClock +import io.jsonwebtoken.impl.DefaultJwtParser import io.jsonwebtoken.impl.FixedClock +import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.lang.DateFormats import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.SignatureException +import org.hamcrest.CoreMatchers import org.junit.Test import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets import java.security.SecureRandom -import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE -import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static io.jsonwebtoken.DateTestUtils.truncateMillis +import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static org.junit.Assert.* +@SuppressWarnings('GrDeprecatedAPIUsage') class JwtParserTest { private static final SecureRandom random = new SecureRandom() //doesn't need to be seeded - just testing @@ -48,23 +53,6 @@ class JwtParserTest { return Encoders.BASE64URL.encode(bytes) } - @Test - void testSetDuplicateSigningKeys() { - - byte[] keyBytes = randomKey() - - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256") - - String compact = Jwts.builder().setPayload('Hello World!').signWith(SignatureAlgorithm.HS256, keyBytes).compact() - - try { - Jwts.parserBuilder().setSigningKey(keyBytes).setSigningKey(key).build().parse(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A key object and key bytes cannot both be specified. Choose either.' - } - } - @Test void testIsSignedWithNullArgument() { assertFalse Jwts.parserBuilder().build().isSigned(null) @@ -83,10 +71,10 @@ class JwtParserTest { String bad = base64Url('{"alg":"none"}') + '.' + base64Url(junkPayload) + '.' try { - Jwts.parserBuilder().build().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload + assertEquals 'Unable to read claims JSON: ' + junkPayload, expected.getMessage() } } @@ -132,7 +120,7 @@ class JwtParserTest { } @Test - void testParsePlaintextJwsWithIncorrectAlg() { + void testParseContentJwsWithIncorrectAlg() { def header = '{"alg":"none"}' @@ -143,14 +131,32 @@ class JwtParserTest { String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { - Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals se.getMessage(), 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.getMessage() } } + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseUnsecuredJwsDefault() { + // not signed - unsecured by default. Parsing should be disabled automatically + def header = '{"alg":"none"}' + def payload = '{"subject":"Joe"}' + String unsecured = base64Url(header) + '.' + base64Url(payload) + '.' + try { + Jwts.parserBuilder().build().parse(unsecured) + fail() + } catch (UnsupportedJwtException expected) { + String msg = DefaultJwtParser.UNSECURED_DISABLED_MSG_PREFIX + '{alg=none}' + assertEquals msg, expected.getMessage() + } + } + @Test void testParseWithBase64EncodedSigningKey() { @@ -165,9 +171,9 @@ class JwtParserTest { assertTrue Jwts.parserBuilder().build().isSigned(compact) - Jwt jwt = Jwts.parserBuilder().setSigningKey(base64Encodedkey).build().parse(compact) + def jwt = Jwts.parserBuilder().setSigningKey(base64Encodedkey).build().parse(compact) - assertEquals jwt.body, payload + assertEquals payload, new String(jwt.payload as byte[], StandardCharsets.UTF_8) } @Test @@ -180,49 +186,48 @@ class JwtParserTest { assertTrue Jwts.parserBuilder().build().isSigned(compact) - Jwt jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact) + def jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact) - assertEquals payload, jwt.body + assertEquals payload, new String(jwt.payload as byte[], StandardCharsets.UTF_8) } @Test void testParseNullPayload() { SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256) - String compact = Jwts.builder().signWith(key).compact() - assertTrue Jwts.parserBuilder().build().isSigned(compact) - Jwt jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact) - - assertEquals '', jwt.body + def jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact) + assertEquals '', new String(jwt.payload as byte[], StandardCharsets.UTF_8) } @Test void testParseNullPayloadWithoutKey() { String compact = Jwts.builder().compact() - - Jwt jwt = Jwts.parserBuilder().build().parse(compact) - + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) assertEquals 'none', jwt.header.alg - assertEquals '', jwt.body + assertEquals '', new String(jwt.payload as byte[], StandardCharsets.UTF_8) } @Test void testParseWithExpiredJwt() { - Date exp = new Date(System.currentTimeMillis() - 1000) + // Test with a fixed clock to assert full exception message + long testTime = 1657552537573L + Clock fixedClock = new FixedClock(testTime) + + Date exp = new Date(testTime - 1000) String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parserBuilder().build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(fixedClock).build().parse(compact) fail() } catch (ExpiredJwtException e) { - assertTrue e.getMessage().startsWith('JWT expired at ') - - //https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): - assertTrue e.getMessage().contains('Z, a difference of ') + // https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): + // https://github.com/jwtk/jjwt/issues/660 (show differences as now - expired) + assertEquals e.getMessage(), "JWT expired at 2022-07-11T15:15:36Z. Current time: " + + "2022-07-11T15:15:37Z, a difference of 1573 milliseconds. Allowed clock skew: 0 milliseconds." } } @@ -234,7 +239,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parserBuilder().build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -251,9 +256,9 @@ class JwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() - Jwt jwt = Jwts.parserBuilder().setAllowedClockSkewSeconds(10).build().parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) - assertEquals jwt.getBody().getSubject(), subject + assertEquals jwt.getPayload().getSubject(), subject } @Test @@ -263,7 +268,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parserBuilder().setAllowedClockSkewSeconds(1).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -277,9 +282,9 @@ class JwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() - Jwt jwt = Jwts.parserBuilder().setAllowedClockSkewSeconds(10).build().parse(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(10).build().parse(compact) - assertEquals jwt.getBody().getSubject(), subject + assertEquals jwt.getPayload().getSubject(), subject } @Test @@ -289,7 +294,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() try { - Jwts.parserBuilder().setAllowedClockSkewSeconds(1).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setAllowedClockSkewSeconds(1).build().parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -297,59 +302,60 @@ class JwtParserTest { } // ======================================================================== - // parsePlaintextJwt tests + // parseContentJwt tests // ======================================================================== @Test - void testParsePlaintextJwt() { + void testParseContentJwt() { String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).compact() - Jwt jwt = Jwts.parserBuilder().build().parsePlaintextJwt(compact) + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) - assertEquals jwt.getBody(), payload + assertEquals payload, new String(jwt.payload, StandardCharsets.UTF_8) } @Test - void testParsePlaintextJwtWithClaimsJwt() { + void testParseContentJwtWithClaimsJwt() { String compact = Jwts.builder().setSubject('Joe').compact() try { - Jwts.parserBuilder().build().parsePlaintextJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals e.getMessage(), 'Unprotected Claims JWTs are not supported.' } } @Test - void testParsePlaintextJwtWithPlaintextJws() { + void testParseContentJwtWithContentJws() { String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parserBuilder().build().parsePlaintextJws(compact) + Jwts.parserBuilder().build().parseContentJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @Test - void testParsePlaintextJwtWithClaimsJws() { + void testParseContentJwtWithClaimsJws() { - String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() + def key = randomKey() + String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parserBuilder().build().parsePlaintextJws(compact) + Jwts.parserBuilder().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -364,28 +370,28 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() - Jwt jwt = Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) - assertEquals jwt.getBody().getSubject(), subject + assertEquals jwt.getPayload().getSubject(), subject } @Test - void testParseClaimsJwtWithPlaintextJwt() { + void testParseClaimsJwtWithContentJwt() { String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @Test - void testParseClaimsJwtWithPlaintextJws() { + void testParseClaimsJwtWithContentJws() { String payload = 'Hello world!' @@ -395,20 +401,21 @@ class JwtParserTest { Jwts.parserBuilder().build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Cannot verify JWS signature: unable to locate signature verification key for JWS with header: {alg=HS256}', e.getMessage() } } @Test void testParseClaimsJwtWithClaimsJws() { - String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() + def key = randomKey() + String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJwt(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -422,7 +429,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parserBuilder().build().parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -437,9 +444,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { - Jwts.parserBuilder(). - build(). - parseClaimsJwt(compact) + Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') @@ -447,11 +452,11 @@ class JwtParserTest { } // ======================================================================== - // parsePlaintextJws tests + // parseContentJws tests // ======================================================================== @Test - void testParsePlaintextJws() { + void testParseContentJws() { String payload = 'Hello world!' @@ -459,16 +464,16 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key).compact() - Jwt jwt = Jwts.parserBuilder(). + def jwt = Jwts.parserBuilder(). setSigningKey(key). build(). - parsePlaintextJws(compact) + parseContentJws(compact) - assertEquals jwt.getBody(), payload + assertEquals payload, new String(jwt.payload, StandardCharsets.UTF_8) } @Test - void testParsePlaintextJwsWithPlaintextJwt() { + void testParseContentJwsWithContentJwt() { String payload = 'Hello world!' @@ -477,15 +482,15 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @Test - void testParsePlaintextJwsWithClaimsJwt() { + void testParseContentJwsWithClaimsJwt() { String subject = 'Joe' @@ -494,15 +499,15 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', e.getMessage() } } @Test - void testParsePlaintextJwsWithClaimsJws() { + void testParseContentJwsWithClaimsJws() { String subject = 'Joe' @@ -511,10 +516,10 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) + Jwts.parserBuilder().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -533,7 +538,7 @@ class JwtParserTest { Jwt jwt = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) - assertEquals jwt.getBody().getSubject(), sub + assertEquals jwt.getPayload().getSubject(), sub } @Test @@ -581,7 +586,7 @@ class JwtParserTest { } @Test - void testParseClaimsJwsWithPlaintextJwt() { + void testParseClaimsJwsWithContentJwt() { String payload = 'Hello world!' @@ -590,10 +595,10 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' + assertEquals 'Unprotected content JWTs are not supported.', e.getMessage() } } @@ -607,17 +612,17 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parserBuilder().setSigningKey(key). + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key). build(). parseClaimsJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', e.getMessage() } } @Test - void testParseClaimsJwsWithPlaintextJws() { + void testParseClaimsJwsWithContentJws() { String subject = 'Joe' @@ -626,10 +631,10 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) + Jwts.parserBuilder().setSigningKey(key).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException e) { - assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' + assertEquals 'Signed Claims JWTs are not supported.', e.getMessage() } } @@ -655,7 +660,7 @@ class JwtParserTest { Jws jws = Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) - assertEquals jws.getBody().getSubject(), subject + assertEquals jws.getPayload().getSubject(), subject } @Test @@ -678,55 +683,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) fail() } catch (SignatureException se) { - assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' - } - } - - @Test - void testParseClaimsWithSigningKeyResolverAndKey() { - - String subject = 'Joe' - - SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") - - String compact = Jwts.builder().setSubject(subject).signWith(key, SignatureAlgorithm.HS256).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parserBuilder().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' - } - } - - @Test - void testParseClaimsWithSigningKeyResolverAndKeyBytes() { - - String subject = 'Joe' - - byte[] key = randomKey() - - String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() - - def signingKeyResolver = new SigningKeyResolverAdapter() { - @Override - byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - return randomKey() - } - } - - try { - Jwts.parserBuilder().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' + assertEquals 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.', se.getMessage() } } @@ -743,7 +700,7 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(null).build().parseClaimsJws(compact) fail() } catch (IllegalArgumentException iae) { - assertEquals iae.getMessage(), 'SigningKeyResolver cannot be null.' + assertEquals 'SigningKeyResolver cannot be null.', iae.getMessage() } } @@ -762,9 +719,9 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseClaimsJws(compact) fail() } catch (UnsupportedJwtException ex) { - assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + + assertEquals 'The specified SigningKeyResolver implementation does not support ' + 'Claims JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, Claims) method ' + - 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.' + 'or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, Claims) method.', ex.getMessage() } } @@ -789,7 +746,7 @@ class JwtParserTest { Jwt jwt = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) - Claims claims = jwt.getBody() + Claims claims = jwt.getPayload() assertEquals(b, claims.get("byte", Byte.class)) assertEquals(s, claims.get("short", Short.class)) @@ -799,11 +756,11 @@ class JwtParserTest { } // ======================================================================== - // parsePlaintextJws with signingKey resolver. + // parseContentJws with signingKey resolver. // ======================================================================== @Test - void testParsePlaintextJwsWithSigningKeyResolverAdapter() { + void testParseContentJwsWithSigningKeyResolverAdapter() { String inputPayload = 'Hello world!' @@ -813,18 +770,18 @@ class JwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() { @Override - byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + byte[] resolveSigningKeyBytes(JwsHeader header, byte[] payload) { return key } } - Jws jws = Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parsePlaintextJws(compact) + def jws = Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseContentJws(compact) - assertEquals jws.getBody(), inputPayload + assertEquals inputPayload, new String(jws.payload, StandardCharsets.UTF_8) } @Test - void testParsePlaintextJwsWithSigningKeyResolverInvalidKey() { + void testParseContentJwsWithSigningKeyResolverInvalidKey() { String inputPayload = 'Hello world!' @@ -834,21 +791,21 @@ class JwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() { @Override - byte[] resolveSigningKeyBytes(JwsHeader header, String payload) { + byte[] resolveSigningKeyBytes(JwsHeader header, byte[] payload) { return randomKey() } } try { - Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parsePlaintextJws(compact) + Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseContentJws(compact) fail() } catch (SignatureException se) { - assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + assertEquals 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.', se.getMessage() } } @Test - void testParsePlaintextJwsWithInvalidSigningKeyResolverAdapter() { + void testParseContentJwsWithInvalidSigningKeyResolverAdapter() { String payload = 'Hello world!' @@ -859,12 +816,12 @@ class JwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() try { - Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parsePlaintextJws(compact) + Jwts.parserBuilder().setSigningKeyResolver(signingKeyResolver).build().parseContentJws(compact) fail() } catch (UnsupportedJwtException ex) { - assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + - 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, String) ' + - 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' + assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support content ' + + 'JWS signing key resolution. Consider overriding either the resolveSigningKey(JwsHeader, byte[]) ' + + 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, byte[]) method.' } } @@ -955,7 +912,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().get(expectedClaimName), expectedClaimValue + assertEquals jwt.getPayload().get(expectedClaimName), expectedClaimValue } @Test @@ -1026,7 +983,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getIssuedAt().getTime(), truncateMillis(issuedAt), 0 + assertEquals jwt.getPayload().getIssuedAt().getTime(), truncateMillis(issuedAt), 0 } @Test(expected = IncorrectClaimException) @@ -1077,7 +1034,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getIssuer(), issuer + assertEquals jwt.getPayload().getIssuer(), issuer } @Test @@ -1144,7 +1101,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getAudience(), audience + assertEquals jwt.getPayload().getAudience(), audience } @Test @@ -1211,7 +1168,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getSubject(), subject + assertEquals jwt.getPayload().getSubject(), subject } @Test @@ -1278,7 +1235,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getId(), id + assertEquals jwt.getPayload().getId(), id } @Test @@ -1346,7 +1303,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getExpiration().getTime(), truncateMillis(expiration) + assertEquals jwt.getPayload().getExpiration().getTime(), truncateMillis(expiration) } @Test(expected = IncorrectClaimException) @@ -1398,7 +1355,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().getNotBefore().getTime(), truncateMillis(notBefore) + assertEquals jwt.getPayload().getNotBefore().getTime(), truncateMillis(notBefore) } @Test(expected = IncorrectClaimException) @@ -1450,7 +1407,7 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getBody().get("aDate", Date.class), aDate + assertEquals jwt.getPayload().get("aDate", Date.class), aDate } @Test @@ -1518,7 +1475,7 @@ class JwtParserTest { try { Jwts.parserBuilder().setSigningKey(key). require("aDate", aDate). - build(). + build(). parseClaimsJws(compact) fail() } catch (MissingClaimException e) { @@ -1537,7 +1494,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() - Jwts.parserBuilder().setClock(new FixedClock(beforeExpiry)).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new FixedClock(beforeExpiry)).build().parse(compact) } @Test @@ -1557,7 +1514,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() try { - Jwts.parserBuilder().setClock(new DefaultClock()).build().parse(compact) + Jwts.parserBuilder().enableUnsecuredJws().setClock(new DefaultClock()).build().parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -1581,7 +1538,8 @@ class JwtParserTest { Jwts.parserBuilder().setSigningKey(randomKey()).build().parse(bad) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '3' + assertEquals expected, se.message } } @@ -1592,10 +1550,12 @@ class JwtParserTest { String jwtStr = '.' + base64Url(payload) + '.' - Jwt jwt = Jwts.parserBuilder().build().parse(jwtStr) - - assertTrue jwt.header == null - assertEquals 'Joe', jwt.body.get('subject') + try { + Jwts.parserBuilder().build().parse(jwtStr) + fail() + } catch (MalformedJwtException e) { + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.getMessage() + } } @Test @@ -1611,7 +1571,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', se.message } } @@ -1627,10 +1587,10 @@ class JwtParserTest { String jwtStr = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(sig) try { - Jwts.parserBuilder().build().parse(jwtStr) + Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwtStr) fail() } catch (MalformedJwtException se) { - assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 009e51e8..edb3d1a6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -15,23 +15,25 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.impl.DefaultHeader -import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.* import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec -import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.impl.security.* +import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings -import io.jsonwebtoken.security.InvalidKeyException -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.WeakKeyException +import io.jsonwebtoken.security.* import org.junit.Test import javax.crypto.Mac +import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.security.Key import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey @@ -41,7 +43,7 @@ import static org.junit.Assert.* class JwtsTest { private static Date now() { - return dateWithOnlySecondPrecision(System.currentTimeMillis()); + return dateWithOnlySecondPrecision(System.currentTimeMillis()) } private static int later() { @@ -74,21 +76,22 @@ class JwtsTest { } @Test - void testSubclass() { + void testPrivateCtor() { // for code coverage only + //noinspection GroovyAccessibility new Jwts() } @Test void testHeaderWithNoArgs() { - def header = Jwts.header() - assertTrue header instanceof DefaultHeader + def header = Jwts.unprotectedHeader() + assertTrue header instanceof DefaultUnprotectedHeader } @Test void testHeaderWithMapArg() { def header = Jwts.header([alg: "HS256"]) - assertTrue header instanceof DefaultHeader - assertEquals header.alg, 'HS256' + assertTrue header instanceof DefaultUnprotectedHeader + assertEquals 'HS256', header.alg } @Test @@ -101,7 +104,7 @@ class JwtsTest { void testJwsHeaderWithMapArg() { def header = Jwts.jwsHeader([alg: "HS256"]) assertTrue header instanceof DefaultJwsHeader - assertEquals header.getAlgorithm(), 'HS256' + assertEquals 'HS256', header.getAlgorithm() } @Test @@ -114,40 +117,106 @@ class JwtsTest { void testClaimsWithMapArg() { Claims claims = Jwts.claims([sub: 'Joe']) assertNotNull claims - assertEquals claims.getSubject(), 'Joe' + assertEquals 'Joe', claims.getSubject() + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseMalformedHeader() { + def headerString = '{"jku":42}' // cannot be parsed as a URI --> malformed header + def claimsString = '{"sub":"joe"}' + def encodedHeader = base64Url(headerString) + def encodedClaims = base64Url(claimsString) + def compact = encodedHeader + '.' + encodedClaims + '.AAD=' + try { + Jwts.parserBuilder().build().parseClaimsJws(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Invalid protected header: Invalid JWS header \'jku\' (JWK Set URL) value: 42. ' + + 'Values must be either String or java.net.URI instances. Value type found: java.lang.Integer.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseMalformedClaims() { + def key = TestKeys.HS256 + def h = base64Url('{"alg":"HS256"}') + def c = base64Url('{"sub":"joe","exp":"-42-"}') + def payload = ("$h.$c" as String).getBytes(StandardCharsets.UTF_8) + def request = new DefaultSecureRequest(payload, null, null, key) + def result = Jwts.SIG.HS256.digest(request) + def sig = Encoders.BASE64URL.encode(result) + def compact = "$h.$c.$sig" as String + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Invalid claims: Invalid JWT Claim \'exp\' (Expiration Time) value: -42-. ' + + 'String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics exhausted. ' + + 'Cause: Unparseable date: "-42-"' + assertEquals expected, e.getMessage() + } } @Test - void testPlaintextJwtString() { - - // Assert exact output per example at https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 - - // The base64url encoding of the example claims set in the spec shows that their original payload ends lines with - // carriage return + newline, so we have to include them in the test payload to assert our encoded output - // matches what is in the spec: - - def payload = '{"iss":"joe",\r\n' + - ' "exp":1300819380,\r\n' + - ' "http://example.com/is_root":true}' - - String val = Jwts.builder().setPayload(payload).compact(); - - def specOutput = 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' - - assertEquals val, specOutput + void testContentJwtString() { + // Assert exact output per example at https://www.rfc-editor.org/rfc/rfc7519.html#section-6.1 + String encodedBody = 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ' + String payload = new String(Decoders.BASE64URL.decode(encodedBody), StandardCharsets.UTF_8) + String val = Jwts.builder().setPayload(payload).compact() + String RFC_VALUE = 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' + assertEquals RFC_VALUE, val } @Test - void testParsePlaintextToken() { + void testSetContentWithContentType() { + String s = 'Hello JJWT' + String cty = 'text/plain' + String compact = Jwts.builder().setContent(s.getBytes(StandardCharsets.UTF_8), cty).compact() + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) + assertEquals cty, jwt.header.getContentType() + assertEquals s, new String(jwt.payload, StandardCharsets.UTF_8) + } - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + @Test + void testSetContentWithApplicationContentType() { + String s = 'Hello JJWT' + String subtype = 'foo' + String cty = "application/$subtype" + String compact = Jwts.builder().setContent(s.getBytes(StandardCharsets.UTF_8), cty).compact() + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) + assertEquals subtype, jwt.header.getContentType() // assert that the compact form was used + assertEquals s, new String(jwt.payload, StandardCharsets.UTF_8) + } - String jwt = Jwts.builder().setClaims(claims).compact(); + @Test + void testSetContentWithNonCompactApplicationContentType() { + String s = 'Hello JJWT' + String subtype = 'foo' + String cty = "application/$subtype;part=1/2" + String compact = Jwts.builder().setContent(s.getBytes(StandardCharsets.UTF_8), cty).compact() + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) + assertEquals cty, jwt.header.getContentType() // two slashes, can't compact + assertEquals s, new String(jwt.payload, StandardCharsets.UTF_8) + } - def token = Jwts.parserBuilder().build().parse(jwt); + @Test + void testParseContentToken() { + + def claims = [iss: 'joe', exp: later(), 'https://example.com/is_root': true] + + String jwt = Jwts.builder().setClaims(claims).compact() + + def token = Jwts.parserBuilder().enableUnsecuredJws().build().parse(jwt) //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims + assert token.payload == claims } @Test(expected = IllegalArgumentException) @@ -165,13 +234,29 @@ class JwtsTest { Jwts.parserBuilder().build().parse(' ') } + @Test + void testParseClaimsWithLeadingAndTrailingWhitespace() { + String whitespaceChars = ' \t \n \r ' + String claimsJson = whitespaceChars + '{"sub":"joe"}' + whitespaceChars + + String header = Encoders.BASE64URL.encode('{"alg":"none"}'.getBytes(StandardCharsets.UTF_8)) + String claims = Encoders.BASE64URL.encode(claimsJson.getBytes(StandardCharsets.UTF_8)) + + String compact = header + '.' + claims + '.' + def jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) + assertEquals 'none', jwt.header.getAlgorithm() + assertEquals 'joe', jwt.payload.getSubject() + } + @Test void testParseWithNoPeriods() { try { Jwts.parserBuilder().build().parse('foo') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 0" + //noinspection GroovyAccessibility + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '0' + assertEquals expected, e.message } } @@ -181,7 +266,9 @@ class JwtsTest { Jwts.parserBuilder().build().parse('.') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 1" + //noinspection GroovyAccessibility + String expected = JwtTokenizer.DELIM_ERR_MSG_PREFIX + '1' + assertEquals expected, e.message } } @@ -191,15 +278,17 @@ class JwtsTest { Jwts.parserBuilder().build().parse('..') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string '..' is missing a header." + 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).' + assertEquals msg, e.message } } @Test void testParseWithHeaderOnly() { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".." - Jwt jwt = Jwts.parserBuilder().build().parse(unsecuredJwt) - assertEquals("none", jwt.getHeader().get("alg")) + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parse(unsecuredJwt) + assertEquals "none", jwt.getHeader().get("alg") } @Test @@ -208,15 +297,29 @@ class JwtsTest { Jwts.parserBuilder().build().parse('..bar') fail() } catch (MalformedJwtException e) { - assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm." + assertEquals 'Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).', e.message + } + } + + @Test + void testParseWithMissingRequiredSignature() { + Key key = Jwts.SIG.HS256.keyBuilder().build() + String compact = Jwts.builder().setSubject('foo').signWith(key).compact() + int i = compact.lastIndexOf('.') + String missingSig = compact.substring(0, i + 1) + try { + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(missingSig) + fail() + } catch (MalformedJwtException expected) { + String s = String.format(DefaultJwtParser.MISSING_JWS_DIGEST_MSG_FMT, 'HS256') + assertEquals s, expected.getMessage() } } @Test void testWithInvalidCompressionAlgorithm() { try { - - Jwts.builder().setHeaderParam(Header.COMPRESSION_ALGORITHM, "CUSTOM").setId("andId").compact() + Jwts.builder().setHeaderParam(AbstractHeader.COMPRESSION_ALGORITHM.getId(), "CUSTOM").setId("andId").compact() } catch (CompressionException e) { assertEquals "Unsupported compression algorithm 'CUSTOM'", e.getMessage() } @@ -224,130 +327,130 @@ class JwtsTest { @Test void testConvenienceIssuer() { - String compact = Jwts.builder().setIssuer("Me").compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims - assertEquals claims.getIssuer(), "Me" + String compact = Jwts.builder().setIssuer("Me").compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims + assertEquals 'Me', claims.getIssuer() compact = Jwts.builder().setSubject("Joe") .setIssuer("Me") //set it .setIssuer(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getIssuer() } @Test void testConvenienceSubject() { - String compact = Jwts.builder().setSubject("Joe").compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims - assertEquals claims.getSubject(), "Joe" + String compact = Jwts.builder().setSubject("Joe").compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims + assertEquals 'Joe', claims.getSubject() compact = Jwts.builder().setIssuer("Me") .setSubject("Joe") //set it .setSubject(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getSubject() } @Test void testConvenienceAudience() { - String compact = Jwts.builder().setAudience("You").compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims - assertEquals claims.getAudience(), "You" + String compact = Jwts.builder().setAudience("You").compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims + assertEquals 'You', claims.getAudience() compact = Jwts.builder().setIssuer("Me") .setAudience("You") //set it .setAudience(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getAudience() } @Test void testConvenienceExpiration() { - Date then = laterDate(10000); - String compact = Jwts.builder().setExpiration(then).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + Date then = laterDate(10000) + String compact = Jwts.builder().setExpiration(then).compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims def claimedDate = claims.getExpiration() - assertEquals claimedDate, then + assertEquals then, claimedDate compact = Jwts.builder().setIssuer("Me") .setExpiration(then) //set it .setExpiration(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getExpiration() } @Test void testConvenienceNotBefore() { Date now = now() //jwt exp only supports *seconds* since epoch: - String compact = Jwts.builder().setNotBefore(now).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + String compact = Jwts.builder().setNotBefore(now).compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims def claimedDate = claims.getNotBefore() - assertEquals claimedDate, now + assertEquals now, claimedDate compact = Jwts.builder().setIssuer("Me") .setNotBefore(now) //set it .setNotBefore(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getNotBefore() } @Test void testConvenienceIssuedAt() { Date now = now() //jwt exp only supports *seconds* since epoch: - String compact = Jwts.builder().setIssuedAt(now).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims + String compact = Jwts.builder().setIssuedAt(now).compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims def claimedDate = claims.getIssuedAt() - assertEquals claimedDate, now + assertEquals now, claimedDate compact = Jwts.builder().setIssuer("Me") .setIssuedAt(now) //set it .setIssuedAt(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getIssuedAt() } @Test void testConvenienceId() { - String id = UUID.randomUUID().toString(); - String compact = Jwts.builder().setId(id).compact(); - Claims claims = Jwts.parserBuilder().build().parse(compact).body as Claims - assertEquals claims.getId(), id + String id = UUID.randomUUID().toString() + String compact = Jwts.builder().setId(id).compact() + Claims claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims + assertEquals id, claims.getId() compact = Jwts.builder().setIssuer("Me") .setId(id) //set it .setId(null) //null should remove it - .compact(); + .compact() - claims = Jwts.parserBuilder().build().parse(compact).body as Claims + claims = Jwts.parserBuilder().enableUnsecuredJws().build().parse(compact).payload as Claims assertNull claims.getId() } @Test void testUncompressedJwt() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) - Claims claims = jws.body + Claims claims = jws.payload assertNull jws.header.getCompressionAlgorithm() @@ -359,17 +462,17 @@ class JwtsTest { @Test void testCompressedJwtWithDeflate() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.DEFLATE).compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) - Claims claims = jws.body + Claims claims = jws.payload assertEquals "DEF", jws.header.getCompressionAlgorithm() @@ -381,17 +484,17 @@ class JwtsTest { @Test void testCompressedJwtWithGZIP() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.GZIP).compact() def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) - Claims claims = jws.body + Claims claims = jws.payload assertEquals "GZIP", jws.header.getCompressionAlgorithm() @@ -403,15 +506,15 @@ class JwtsTest { @Test void testCompressedWithCustomResolver() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -420,6 +523,7 @@ class JwtsTest { @Override CompressionCodec resolveCompressionCodec(Header header) { String algorithm = header.getCompressionAlgorithm() + //noinspection ChangeToOperator if ("CUSTOM".equals(algorithm)) { return CompressionCodecs.GZIP } else { @@ -428,7 +532,7 @@ class JwtsTest { } }).build().parseClaimsJws(compact) - Claims claims = jws.body + Claims claims = jws.payload assertEquals "CUSTOM", jws.header.getCompressionAlgorithm() @@ -441,15 +545,15 @@ class JwtsTest { @Test(expected = CompressionException.class) void testCompressedJwtWithUnrecognizedHeader() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override - String getAlgorithmName() { + String getId() { return "CUSTOM" } }).compact() @@ -460,122 +564,135 @@ class JwtsTest { @Test void testCompressStringPayloadWithDeflate() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String payload = "this is my test for a payload" - String compact = Jwts.builder().setPayload(payload).signWith(alg, key) + String compact = Jwts.builder().setPayload(payload).signWith(key, alg) .compressWith(CompressionCodecs.DEFLATE).compact() - def jws = Jwts.parserBuilder().setSigningKey(key).build().parsePlaintextJws(compact) - - String parsed = jws.body + def jws = Jwts.parserBuilder().setSigningKey(key).build().parseContentJws(compact) assertEquals "DEF", jws.header.getCompressionAlgorithm() - assertEquals "this is my test for a payload", parsed + assertEquals "this is my test for a payload", new String(jws.payload, StandardCharsets.UTF_8) } @Test void testHS256() { - testHmac(SignatureAlgorithm.HS256) + testHmac(Jwts.SIG.HS256) } @Test void testHS384() { - testHmac(SignatureAlgorithm.HS384) + testHmac(Jwts.SIG.HS384) } @Test void testHS512() { - testHmac(SignatureAlgorithm.HS512) + testHmac(Jwts.SIG.HS512) } @Test void testRS256() { - testRsa(SignatureAlgorithm.RS256) + testRsa(Jwts.SIG.RS256) } @Test void testRS384() { - testRsa(SignatureAlgorithm.RS384) + testRsa(Jwts.SIG.RS384) } @Test void testRS512() { - testRsa(SignatureAlgorithm.RS512) + testRsa(Jwts.SIG.RS512) } @Test void testPS256() { - testRsa(SignatureAlgorithm.PS256) + testRsa(Jwts.SIG.PS256) } @Test void testPS384() { - testRsa(SignatureAlgorithm.PS384) + testRsa(Jwts.SIG.PS384) } @Test void testPS512() { - testRsa(SignatureAlgorithm.PS512) + testRsa(Jwts.SIG.PS512) } @Test void testRSA256WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS256, true) + testRsa(Jwts.SIG.RS256, true) } @Test void testRSA384WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS384, true) + testRsa(Jwts.SIG.RS384, true) } @Test void testRSA512WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS512, true) + testRsa(Jwts.SIG.RS512, true) } @Test void testES256() { - testEC(SignatureAlgorithm.ES256) + testEC(Jwts.SIG.ES256) } @Test void testES384() { - testEC(SignatureAlgorithm.ES384) + testEC(Jwts.SIG.ES384) } @Test void testES512() { - testEC(SignatureAlgorithm.ES512) + testEC(Jwts.SIG.ES512) + } + + @Test + void testEdDSA() { + testEC(Jwts.SIG.EdDSA) + } + + @Test + void testEd25519() { + testEC(Jwts.SIG.Ed25519) + } + + @Test + void testEd448() { + testEC(Jwts.SIG.Ed448) } @Test void testES256WithPrivateKeyValidation() { + def alg = Jwts.SIG.ES256 try { - testEC(SignatureAlgorithm.ES256, true) + testEC(alg, true) fail("EC private keys cannot be used to validate EC signatures.") } catch (UnsupportedJwtException e) { - assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey instance." + String msg = "${alg.getId()} verification keys must be PublicKeys (implement java.security.PublicKey). " + + "Provided key type: sun.security.ec.ECPrivateKeyImpl." + assertEquals msg, e.cause.message } } - @Test + @Test(expected = WeakKeyException) void testParseClaimsJwsWithWeakHmacKey() { - SignatureAlgorithm alg = SignatureAlgorithm.HS384 - def key = Keys.secretKeyFor(alg) - def weakKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) + def alg = Jwts.SIG.HS384 + def key = alg.keyBuilder().build() + def weakKey = Jwts.SIG.HS256.keyBuilder().build() String jws = Jwts.builder().setSubject("Foo").signWith(key, alg).compact() - try { - Jwts.parserBuilder().setSigningKey(weakKey).build().parseClaimsJws(jws) - fail('parseClaimsJws must fail for weak keys') - } catch (WeakKeyException expected) { - } + Jwts.parserBuilder().setSigningKey(weakKey).build().parseClaimsJws(jws) + fail('parseClaimsJws must fail for weak keys') } /** @@ -584,7 +701,7 @@ class JwtsTest { @Test void testBuilderWithEcdsaPublicKey() { def builder = Jwts.builder().setSubject('foo') - def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) + def pair = TestKeys.ES256.pair try { builder.signWith(pair.public, SignatureAlgorithm.ES256) //public keys can't be used to create signatures } catch (InvalidKeyException expected) { @@ -599,13 +716,14 @@ class JwtsTest { @Test void testBuilderWithMismatchedEllipticCurveKeyAndAlgorithm() { def builder = Jwts.builder().setSubject('foo') - def pair = Keys.keyPairFor(SignatureAlgorithm.ES384) + def pair = TestKeys.ES384.pair try { - builder.signWith(pair.private, SignatureAlgorithm.ES256) //ES384 keys can't be used to create ES256 signatures + builder.signWith(pair.private, SignatureAlgorithm.ES256) + //ES384 keys can't be used to create ES256 signatures } catch (InvalidKeyException expected) { String msg = "EllipticCurve key has a field size of 48 bytes (384 bits), but ES256 requires a " + "field size of 32 bytes (256 bits) per [RFC 7518, Section 3.4 (validation)]" + - "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." + "(https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." assertEquals msg, expected.getMessage() } } @@ -615,9 +733,9 @@ class JwtsTest { */ @Test void testParserWithMismatchedEllipticCurveKeyAndAlgorithm() { - def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) + def pair = TestKeys.ES256.pair def jws = Jwts.builder().setSubject('foo').signWith(pair.private).compact() - def parser = Jwts.parserBuilder().setSigningKey(Keys.keyPairFor(SignatureAlgorithm.ES384).public).build() + def parser = Jwts.parserBuilder().setSigningKey(TestKeys.ES384.pair.public).build() try { parser.parseClaimsJws(jws) } catch (UnsupportedJwtException expected) { @@ -634,12 +752,12 @@ class JwtsTest { /** * @since 0.11.5 as part of testing guards against JVM CVE-2022-21449 */ - @Test(expected=io.jsonwebtoken.security.SignatureException) + @Test(expected = io.jsonwebtoken.security.SignatureException) void testEcdsaInvalidSignatureValue() { def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" String jws = withoutSignature + '.' + invalidEncodedSignature - def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) + def keypair = Jwts.SIG.ES256.keyPairBuilder().build() Jwts.parserBuilder().setSigningKey(keypair.public).build().parseClaimsJws(jws) } @@ -648,29 +766,422 @@ class JwtsTest { void testParseClaimsJwsWithUnsignedJwt() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() String notSigned = Jwts.builder().setSubject("Foo").compact() try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(notSigned) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parseClaimsJws(notSigned) fail('parseClaimsJws must fail for unsigned JWTs') } catch (UnsupportedJwtException expected) { - assertEquals expected.message, 'Unsigned Claims JWTs are not supported.' + assertEquals 'Unprotected Claims JWTs are not supported.', expected.message } } + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweMissingAlg() { + def h = base64Url('{"enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_JWE_ALG_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweEmptyAlg() { + def h = base64Url('{"alg":"","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_JWE_ALG_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWhitespaceAlg() { + def h = base64Url('{"alg":" ","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_JWE_ALG_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithNoneAlg() { + def h = base64Url('{"alg":"none","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.JWE_NONE_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingAadTag() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.ecek.iv.' + c + '.' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = String.format(DefaultJwtParser.MISSING_JWE_DIGEST_MSG_FMT, 'dir') + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithEmptyAadTag() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + // our decoder skips invalid Base64Url characters, so this decodes to empty which is not allowed: + def tag = '&' + def compact = h + '.IA==.IA==.' + c + '.' + tag + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE strings must always contain an AAD Authentication Tag.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingRequiredBody() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def compact = h + '.ecek.iv..tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE strings MUST always contain a payload (ciphertext).' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithEmptyEncryptedKey() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + // our decoder skips invalid Base64Url characters, so this decodes to empty which is not allowed: + def encodedKey = '&' + def compact = h + '.' + encodedKey + '.iv.' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE string represents an encrypted key, but the key is empty.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingInitializationVector() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def compact = h + '.IA==..' + c + '.tag' + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + String expected = 'Compact JWE strings must always contain an Initialization Vector.' + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithMissingEncHeader() { + def h = base64Url('{"alg":"dir"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (MalformedJwtException e) { + assertEquals DefaultJwtParser.MISSING_ENC_MSG, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithUnrecognizedEncValue() { + def h = base64Url('{"alg":"dir","enc":"foo"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (UnsupportedJwtException e) { + String expected = "Unrecognized JWE 'enc' (Encryption Algorithm) header value: foo" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithUnrecognizedAlgValue() { + def h = base64Url('{"alg":"bar","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (UnsupportedJwtException e) { + String expected = "Unrecognized JWE 'alg' (Algorithm) header value: bar" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJwsWithUnrecognizedAlgValue() { + def h = base64Url('{"alg":"bar"}') + def c = base64Url('{"sub":"joe"}') + def sig = 'IA==' + def compact = "$h.$c.$sig" as String + try { + Jwts.parserBuilder().build().parseClaimsJws(compact) + fail() + } catch (io.jsonwebtoken.security.SignatureException e) { + String expected = "Unsupported signature algorithm 'bar'" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithUnlocatableKey() { + def h = base64Url('{"alg":"dir","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + try { + Jwts.parserBuilder().build().parseClaimsJwe(compact) + fail() + } catch (UnsupportedJwtException e) { + String expected = "Cannot decrypt JWE payload: unable to locate key for JWE with header: {alg=dir, enc=A128GCM}" + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJwsWithCustomSignatureAlgorithm() { + def realAlg = Jwts.SIG.HS256 // any alg will do, we're going to wrap it + def key = TestKeys.HS256 + def id = realAlg.getId() + 'X' // custom id + def alg = new MacAlgorithm() { + @Override + SecretKeyBuilder keyBuilder() { + return realAlg.keyBuilder() + } + + @Override + int getKeyBitLength() { + return realAlg.keyBitLength + } + + @Override + byte[] digest(SecureRequest request) { + return realAlg.digest(request) + } + + @Override + boolean verify(VerifySecureDigestRequest request) { + return realAlg.verify(request) + } + + @Override + String getId() { + return id + } + } + + def jws = Jwts.builder().setSubject("joe").signWith(key, alg).compact() + + assertEquals 'joe', Jwts.parserBuilder() + .addSignatureAlgorithms([alg]) + .setSigningKey(key) + .build() + .parseClaimsJws(jws).payload.getSubject() + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithCustomEncryptionAlgorithm() { + def realAlg = Jwts.ENC.A128GCM // any alg will do, we're going to wrap it + def key = realAlg.keyBuilder().build() + def enc = realAlg.getId() + 'X' // custom id + def encAlg = new AeadAlgorithm() { + @Override + AeadResult encrypt(AeadRequest request) throws SecurityException { + return realAlg.encrypt(request) + } + + @Override + Message decrypt(DecryptAeadRequest request) throws SecurityException { + return realAlg.decrypt(request) + } + + @Override + String getId() { + return enc + } + + @Override + SecretKeyBuilder keyBuilder() { + return realAlg.keyBuilder() + } + + @Override + int getKeyBitLength() { + return realAlg.getKeyBitLength() + } + } + + def jwe = Jwts.builder().setSubject("joe").encryptWith(key, encAlg).compact() + + assertEquals 'joe', Jwts.parserBuilder() + .addEncryptionAlgorithms([encAlg]) + .decryptWith(key) + .build() + .parseClaimsJwe(jwe).payload.getSubject() + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseJweWithBadKeyAlg() { + def alg = 'foo' + def h = base64Url('{"alg":"foo","enc":"A128GCM"}') + def c = base64Url('{"sub":"joe"}') + def ekey = 'IA==' + def iv = 'IA==' + def tag = 'IA==' + def compact = "$h.$ekey.$iv.$c.$tag" as String + + def badKeyAlg = new KeyAlgorithm() { + @Override + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + return null + } + + @Override + SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException { + return null // bad implementation here - returns null, and that's not good + } + + @Override + String getId() { + return alg + } + } + + try { + Jwts.parserBuilder() + .setKeyLocator(new ConstantKeyLocator(TestKeys.HS256, TestKeys.A128GCM)) + .addKeyAlgorithms([badKeyAlg]) // <-- add bad alg here + .build() + .parseClaimsJwe(compact) + fail() + } catch (IllegalStateException e) { + String expected = "The 'foo' JWE key algorithm did not return a decryption key. " + + "Unable to perform 'A128GCM' decryption." + assertEquals expected, e.getMessage() + } + } + + /** + * @since JJWT_RELEASE_VERSION + */ + @Test + void testParseRequiredInt() { + def key = TestKeys.HS256 + def jws = Jwts.builder().signWith(key).claim("foo", 42).compact() + Jwts.parserBuilder().setSigningKey(key) + .require("foo", 42L) //require a long, but jws contains int, should still work + .build().parseClaimsJws(jws) + } + //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @Test void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded + def alg = Jwts.SIG.HS256 + SecretKey key = alg.keyBuilder().build() //this is a 'real', valid JWT: - String compact = Jwts.builder().setSubject("Joe").signWith(alg, key).compact() + String compact = Jwts.builder().setSubject("Joe").signWith(key, alg).compact() //Now strip off the signature so we can add it back in later on a forged token: int i = compact.lastIndexOf('.') @@ -680,17 +1191,17 @@ class JwtsTest { String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: - assertEquals Jwts.parserBuilder().build().parseClaimsJwt(forged).getHeader().get('alg'), 'none' + assertEquals 'none', Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(forged).getHeader().get('alg') //now let's forge it by appending the signature the server expects: forged += signature //now assert that, when the server tries to parse the forged token, parsing fails: try { - Jwts.parserBuilder().setSigningKey(key).build().parse(forged) + Jwts.parserBuilder().enableUnsecuredJws().setSigningKey(key).build().parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { - assertEquals expected.message, 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' + assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', expected.message } } @@ -699,7 +1210,7 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + KeyPair kp = TestKeys.RS256.pair PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() @@ -709,17 +1220,17 @@ class JwtsTest { // Now for the forgery: simulate an attacker using the RSA public key to sign a token, but // using it as an HMAC signing key instead of RSA: - Mac mac = Mac.getInstance('HmacSHA256'); - mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); + Mac mac = Mac.getInstance('HmacSHA256') + mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')) byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) String encodedSignature = Encoders.BASE64URL.encode(signatureBytes) //Finally, the forged token is the header + body + forged signature: - String forged = compact + encodedSignature; + String forged = compact + encodedSignature // Assert that the server (that should always use the private key) does not recognized the forged token: try { - Jwts.parserBuilder().setSigningKey(privateKey).build().parse(forged); + Jwts.parserBuilder().setSigningKey(privateKey).build().parse(forged) fail("Forged token must not be successfully parsed.") } catch (UnsupportedJwtException expected) { assertTrue expected.getMessage().startsWith('The parsed JWT indicates it was signed with the') @@ -731,8 +1242,8 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) - PublicKey publicKey = kp.getPublic(); + KeyPair kp = TestKeys.RS256.pair + PublicKey publicKey = kp.getPublic() //PrivateKey privateKey = kp.getPrivate(); String header = base64Url(toJson(['alg': 'HS256'])) @@ -741,45 +1252,13 @@ class JwtsTest { // Now for the forgery: simulate an attacker using the RSA public key to sign a token, but // using it as an HMAC signing key instead of RSA: - Mac mac = Mac.getInstance('HmacSHA256'); - mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); + Mac mac = Mac.getInstance('HmacSHA256') + mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')) byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = Encoders.BASE64URL.encode(signatureBytes); + String encodedSignature = Encoders.BASE64URL.encode(signatureBytes) //Finally, the forged token is the header + body + forged signature: - String forged = compact + encodedSignature; - - // Assert that the parser does not recognized the forged token: - try { - Jwts.parserBuilder().setSigningKey(publicKey).build().parse(forged); - fail("Forged token must not be successfully parsed.") - } catch (UnsupportedJwtException expected) { - assertTrue expected.getMessage().startsWith('The parsed JWT indicates it was signed with the') - } - } - - //Asserts correct behavior for https://github.com/jwtk/jjwt/issues/25 - @Test - void testParseForgedEllipticCurvePublicKeyAsHmacToken() { - - //Create a legitimate RSA public and private key pair: - KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.ES256) - PublicKey publicKey = kp.getPublic(); - //PrivateKey privateKey = kp.getPrivate(); - - String header = base64Url(toJson(['alg': 'HS256'])) - String body = base64Url(toJson('foo')) - String compact = header + '.' + body + '.' - - // Now for the forgery: simulate an attacker using the Elliptic Curve public key to sign a token, but - // using it as an HMAC signing key instead of Elliptic Curve: - Mac mac = Mac.getInstance('HmacSHA256'); - mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); - byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = Encoders.BASE64URL.encode(signatureBytes); - - //Finally, the forged token is the header + body + forged signature: - String forged = compact + encodedSignature; + String forged = compact + encodedSignature // Assert that the parser does not recognized the forged token: try { @@ -790,13 +1269,320 @@ class JwtsTest { } } - static void testRsa(SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { + //Asserts correct behavior for https://github.com/jwtk/jjwt/issues/25 + @Test + void testParseForgedEllipticCurvePublicKeyAsHmacToken() { - KeyPair kp = Keys.keyPairFor(alg) + //Create a legitimate EC public and private key pair: + KeyPair kp = TestKeys.ES256.pair + PublicKey publicKey = kp.getPublic() + //PrivateKey privateKey = kp.getPrivate(); + + String header = base64Url(toJson(['alg': 'HS256'])) + String body = base64Url(toJson('foo')) + String compact = header + '.' + body + '.' + + // Now for the forgery: simulate an attacker using the Elliptic Curve public key to sign a token, but + // using it as an HMAC signing key instead of Elliptic Curve: + Mac mac = Mac.getInstance('HmacSHA256') + mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')) + byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) + String encodedSignature = Encoders.BASE64URL.encode(signatureBytes) + + //Finally, the forged token is the header + body + forged signature: + String forged = compact + encodedSignature + + // Assert that the parser does not recognized the forged token: + try { + Jwts.parserBuilder().setSigningKey(publicKey).build().parse(forged) + fail("Forged token must not be successfully parsed.") + } catch (UnsupportedJwtException expected) { + assertTrue expected.getMessage().startsWith('The parsed JWT indicates it was signed with the') + } + } + + @Test + void testSecretKeyJwes() { + + def algs = Jwts.KEY.values().findAll({ it -> + it instanceof DirectKeyAlgorithm || it instanceof SecretKeyAlgorithm + })// as Collection> + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : Jwts.ENC.values()) { + + SecretKey key = alg instanceof SecretKeyAlgorithm ? + ((SecretKeyAlgorithm) alg).keyBuilder().build() : + enc.keyBuilder().build() + + // encrypt: + String jwe = Jwts.builder() + .claim('foo', 'bar') + .encryptWith(key, alg, enc) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getPayload().get('foo') + } + } + } + + @Test + void testJweCompression() { + + def codecs = [CompressionCodecs.DEFLATE, CompressionCodecs.GZIP] + + for (CompressionCodec codec : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.values()) { + + SecretKey key = enc.keyBuilder().build() + + // encrypt and compress: + String jwe = Jwts.builder() + .claim('foo', 'bar') + .compressWith(codec) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getPayload().get('foo') + } + } + } + + @Test + void testPasswordJwes() { + + def algs = Jwts.KEY.values().findAll({ it -> + it instanceof Pbes2HsAkwAlgorithm + })// as Collection> + + Password key = Keys.forPassword("12345678".toCharArray()) + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : Jwts.ENC.values()) { + + // encrypt: + String jwe = Jwts.builder() + .claim('foo', 'bar') + .encryptWith(key, alg, enc) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getPayload().get('foo') + } + } + } + + @Test + void testPasswordJweWithoutSpecifyingAlg() { + + Password key = Keys.forPassword("12345678".toCharArray()) + + // encrypt: + String jwe = Jwts.builder() + .claim('foo', 'bar') + .encryptWith(key, Jwts.ENC.A256GCM) // should auto choose KeyAlg PBES2_HS512_A256KW + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(key) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getPayload().get('foo') + assertEquals Jwts.KEY.PBES2_HS512_A256KW, Jwts.KEY.get(jwt.getHeader().getAlgorithm()) + } + + @Test + void testRsaJwes() { + + def pairs = [TestKeys.RS256.pair, TestKeys.RS384.pair, TestKeys.RS512.pair] + + def algs = Jwts.KEY.values().findAll({ it -> + it instanceof DefaultRsaKeyAlgorithm + })// as Collection> + + for (KeyPair pair : pairs) { + + def pubKey = pair.getPublic() + def privKey = pair.getPrivate() + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : Jwts.ENC.values()) { + + // encrypt: + String jwe = Jwts.builder() + .claim('foo', 'bar') + .encryptWith(pubKey, alg, enc) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(privKey) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getPayload().get('foo') + } + } + } + } + + @Test + void testEcJwes() { + + def pairs = [TestKeys.ES256.pair, TestKeys.ES384.pair, TestKeys.ES512.pair] + + def algs = Jwts.KEY.values().findAll({ it -> + it.getId().startsWith("ECDH-ES") + }) + + for (KeyPair pair : pairs) { + + def pubKey = pair.getPublic() + def privKey = pair.getPrivate() + + for (KeyAlgorithm alg : algs) { + + for (AeadAlgorithm enc : Jwts.ENC.values()) { + + // encrypt: + String jwe = Jwts.builder() + .claim('foo', 'bar') + .encryptWith(pubKey, alg, enc) + .compact() + + //decrypt: + def jwt = Jwts.parserBuilder() + .decryptWith(privKey) + .build() + .parseClaimsJwe(jwe) + assertEquals 'bar', jwt.getPayload().get('foo') + } + } + } + } + + @Test + void testEdwardsCurveJwes() { // ensures encryption works with Edwards Curve keys (X25519 and X448) + + def pairs = [TestKeys.X25519.pair, TestKeys.X448.pair] + + def algs = Jwts.KEY.values().findAll({ it -> + it.getId().startsWith("ECDH-ES") + }) + + for (KeyPair pair : pairs) { + + def pubKey = pair.getPublic() + def privKey = pair.getPrivate() + + for (KeyAlgorithm alg : algs) { + for (AeadAlgorithm enc : Jwts.ENC.values()) { + String jwe = encrypt(pubKey, alg, enc) + def jwt = decrypt(jwe, privKey) + assertEquals 'bar', jwt.getPayload().get('foo') + } + } + } + } + + /** + * Asserts that Edwards Curve signing keys cannot be used for encryption (key agreement) per + * https://www.rfc-editor.org/rfc/rfc8037#section-3.1 + */ + @Test + void testEdwardsCurveEncryptionWithSigningKeys() { + def pairs = [TestKeys.Ed25519.pair, TestKeys.Ed448.pair] // signing keys, can't be used + + def algs = Jwts.KEY.values().findAll({ it -> + it.getId().startsWith("ECDH-ES") + }) + + for (KeyPair pair : pairs) { + def pubKey = pair.getPublic() + for (KeyAlgorithm alg : algs) { + for (AeadAlgorithm enc : Jwts.ENC.values()) { + try { + encrypt(pubKey, alg, enc) + fail() + } catch (UnsupportedKeyException expected) { + String id = EdwardsCurve.forKey(pubKey).getId() + String msg = id + " keys may not be used with ECDH-ES key " + + "agreement algorithms per https://www.rfc-editor.org/rfc/rfc8037#section-3.1" + assertEquals msg, expected.getMessage() + } + } + } + } + } + + /** + * Asserts that Edwards Curve signing keys cannot be used for decryption (key agreement) per + * https://www.rfc-editor.org/rfc/rfc8037#section-3.1 + */ + @Test + void testEdwardsCurveDecryptionWithSigningKeys() { + + def pairs = [ // private keys are invalid signing keys to test decryption: + new KeyPair(TestKeys.X25519.pair.public, TestKeys.Ed25519.pair.private), + new KeyPair(TestKeys.X448.pair.public, TestKeys.Ed448.pair.private) + ] + + def algs = Jwts.KEY.values().findAll({ it -> + it.getId().startsWith("ECDH-ES") + }) + + for(KeyPair pair : pairs) { + for (KeyAlgorithm alg : algs) { + for (AeadAlgorithm enc : Jwts.ENC.values()) { + String jwe = encrypt(pair.getPublic(), alg, enc) + PrivateKey key = pair.getPrivate() + try { + decrypt(jwe, key) // invalid signing key + fail() + } catch (UnsupportedKeyException expected) { + String id = EdwardsCurve.forKey(key).getId() + String msg = id + " keys may not be used with ECDH-ES key " + + "agreement algorithms per https://www.rfc-editor.org/rfc/rfc8037#section-3.1" + assertEquals msg, expected.getMessage() + } + } + } + } + } + + static String encrypt(PublicKey key, KeyAlgorithm alg, AeadAlgorithm enc) { + return Jwts.builder().claim('foo', 'bar').encryptWith(key, alg, enc).compact() + } + + static Jwe decrypt(String jwe, PrivateKey key) { + return Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(jwe) + } + + static void testRsa(io.jsonwebtoken.security.SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { + + KeyPair kp = TestKeys.forAlgorithm(alg).pair PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) String jwt = Jwts.builder().setClaims(claims).signWith(privateKey, alg).compact() @@ -805,36 +1591,34 @@ class JwtsTest { key = privateKey } - def token = Jwts.parserBuilder().setSigningKey(key).build().parse(jwt) + def token = Jwts.parserBuilder().verifyWith(key).build().parse(jwt) - assert [alg: alg.name()] == token.header - //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims + assertEquals([alg: alg.getId()], token.header) + assertEquals(claims, token.payload) } - static void testHmac(SignatureAlgorithm alg) { + static void testHmac(MacAlgorithm alg) { //create random signing key for testing: - byte[] key = Keys.secretKeyFor(alg).encoded + SecretKey key = alg.keyBuilder().build() - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) - String jwt = Jwts.builder().setClaims(claims).signWith(alg, key).compact() + String jwt = Jwts.builder().setClaims(claims).signWith(key, alg).compact() - def token = Jwts.parserBuilder().setSigningKey(key).build().parse(jwt) + def token = Jwts.parserBuilder().verifyWith(key).build().parse(jwt) - assert token.header == [alg: alg.name()] - //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims + assertEquals([alg: alg.getId()], token.header) + assertEquals(claims, token.payload) } - static void testEC(SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { + static void testEC(io.jsonwebtoken.security.SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair pair = Keys.keyPairFor(alg) + KeyPair pair = TestKeys.forAlgorithm(alg).pair PublicKey publicKey = pair.getPublic() PrivateKey privateKey = pair.getPrivate() - def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] + def claims = new DefaultClaims([iss: 'joe', exp: later(), 'https://example.com/is_root': true]) String jwt = Jwts.builder().setClaims(claims).signWith(privateKey, alg).compact() @@ -843,11 +1627,10 @@ class JwtsTest { key = privateKey } - def token = Jwts.parserBuilder().setSigningKey(key).build().parse(jwt) + def token = Jwts.parserBuilder().verifyWith(key).build().parse(jwt) - assert token.header == [alg: alg.name()] - //noinspection GrEqualsBetweenInconvertibleTypes - assert token.body == claims + assertEquals([alg: alg.getId()], token.header) + assertEquals(claims, token.payload) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy new file mode 100644 index 00000000..9a6e19a5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/LocatorAdapterTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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 + +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.DefaultUnprotectedHeader +import org.junit.Test + +import static org.junit.Assert.assertNull +import static org.junit.Assert.assertSame + +class LocatorAdapterTest { + + @Test + void testJwtHeader() { + Header input = new DefaultUnprotectedHeader() + def locator = new LocatorAdapter() { + @Override + protected Object locate(UnprotectedHeader header) { + return header + } + } + assertSame input, locator.locate(input as Header /* force Groovy to avoid signature erasure */) + } + + @Test + void testJwtHeaderWithoutOverride() { + Header input = new DefaultUnprotectedHeader() + Locator locator = new LocatorAdapter() {} + assertNull locator.locate(input as Header /* force Groovy to avoid signature erasure */) + } + + @Test + void testJwsHeader() { + Header input = new DefaultJwsHeader() + Locator locator = new LocatorAdapter() { + @Override + protected Object locate(JwsHeader header) { + return header + } + } + assertSame input, locator.locate(input as Header /* force Groovy to avoid signature erasure */) + } + + @Test + void testJwsHeaderWithoutOverride() { + Header input = new DefaultJwsHeader() + Locator locator = new LocatorAdapter() {} + assertNull locator.locate(input as Header) + } + + @Test + void testJweHeader() { + JweHeader input = new DefaultJweHeader() + def locator = new LocatorAdapter() { + @Override + protected Object locate(JweHeader header) { + return header + } + } + assertSame input, locator.locate(input as Header) + } + + @Test + void testJweHeaderWithoutOverride() { + JweHeader input = new DefaultJweHeader() + def locator = new LocatorAdapter() {} + assertNull locator.locate(input as Header) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy index 15dff546..8f653c42 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy @@ -35,7 +35,7 @@ class RsaSigningKeyResolverAdapterTest { Jws jws = Jwts.parserBuilder().setSigningKey(pair.public).build().parseClaimsJws(compact) try { - new SigningKeyResolverAdapter().resolveSigningKey(jws.header, jws.body) + new SigningKeyResolverAdapter().resolveSigningKey(jws.header, jws.payload) fail() } catch (IllegalArgumentException iae) { assertEquals "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be used for asymmetric key algorithms (RSA, Elliptic Curve). Override the resolveSigningKey(JwsHeader, Claims) method instead and return a Key instance appropriate for the RS256 algorithm.", iae.message diff --git a/api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy similarity index 100% rename from api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractHeaderTest.groovy new file mode 100644 index 00000000..28a8f923 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractHeaderTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 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.Header +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class AbstractHeaderTest { + + private AbstractHeader header + + @Before + void setUp() { + header = new AbstractHeader(AbstractHeader.FIELDS){} + } + + @Test + void testType() { + header.setType('foo') + assertEquals header.getType(), 'foo' + } + + @Test + void testContentType() { + header.setContentType('bar') + assertEquals 'bar', header.getContentType() + assertEquals 'bar', header.get('cty') + } + + @Test + void testAlgorithm() { + header.setAlgorithm('foo') + assertEquals 'foo', header.getAlgorithm() + + header = new AbstractHeader(AbstractHeader.FIELDS, [alg: 'bar']){} + assertEquals 'bar', header.getAlgorithm() + } + + @Test + void testSetCompressionAlgorithm() { + header.setCompressionAlgorithm("DEF") + assertEquals "DEF", header.getCompressionAlgorithm() + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + @Test + void testBackwardsCompatibleCompressionHeader() { + header.put(Header.DEPRECATED_COMPRESSION_ALGORITHM, "DEF") + assertEquals "DEF", header.getCompressionAlgorithm() + } + + @Test + void testGetName() { + def header = new AbstractHeader(AbstractHeader.FIELDS){} + assertEquals 'JWT header', header.getName() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy new file mode 100644 index 00000000..54bcaacb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2022 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.impl.security.Randoms +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.SecretJwk +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.* + +class AbstractProtectedHeaderTest { + + private AbstractProtectedHeader header + + @Before + void setUp() { + header = new AbstractProtectedHeader(AbstractProtectedHeader.FIELDS) {} + } + + @Test + void testKeyId() { + def kid = 'foo' + header.setKeyId(kid) + assertEquals kid, header.get('kid') + assertEquals kid, header.getKeyId() + } + + @Test + void testKeyIdNonString() { + try { + header.put('kid', 42) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'kid' (Key ID) value: 42. Unsupported value type. " + + "Expected: java.lang.String, found: java.lang.Integer" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testSetJku() { + URI uri = URI.create('https://github.com') + header.setJwkSetUrl(uri) + assertEquals uri.toString(), header.get('jku') + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testPutJkuUri() { + URI uri = URI.create('https://google.com') + header.put('jku', uri) + assertEquals uri.toString(), header.get('jku') + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testPutJkuString() { + String url = 'https://google.com' + URI uri = URI.create(url) + header.put('jku', url) + assertEquals url, header.get('jku') + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testPutJkuNonString() { + try { + header.put('jku', 42) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'jku' (JWK Set URL) value: 42. Values must be either String or " + + "java.net.URI instances. Value type found: java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithNull() { + header.put('jwk', null) + assertNull header.getJwk() + } + + @Test + void testJwkWithEmptyMap() { + header.put('jwk', [:]) + assertNull header.getJwk() + } + + @Test + void testJwkWithoutMap() { + try { + header.put('jwk', 42) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: 42. " + + "Value must be a Jwk or Map. Type found: java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithJwk() { + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() + EcPublicJwk pubJwk = jwk.toPublicJwk() + header.setJwk(pubJwk) + assertEquals pubJwk, header.getJwk() + } + + @Test + void testJwkWithMap() { + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() + EcPublicJwk pubJwk = jwk.toPublicJwk() + Map m = new LinkedHashMap<>(pubJwk) + header.put('jwk', m) + assertEquals pubJwk, header.getJwk() + } + + @Test + void testJwkWithBadMapKeys() { + def m = [42: "hello"] + try { + header.put('jwk', m) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {42=hello}. JWK map keys must be Strings. " + + "Encountered key '42' of type java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithSecretJwk() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() + try { + header.put('jwk', jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {kty=oct, k=}. " + + "Value must be a Public JWK, not a Secret JWK." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testJwkWithPrivateJwk() { + EcPrivateJwk jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).build() + try { + header.put('jwk', jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {kty=EC, crv=P-256, " + + "x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, " + + "d=}. Value must be a Public JWK, not an EC Private JWK." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testX509Url() { + URI uri = URI.create('https://google.com') + header.setX509Url(uri) + assertEquals uri, header.getX509Url() + } + + @Test + void testX509UrlString() { //test canonical/idiomatic conversion + String url = 'https://google.com' + URI uri = URI.create(url) + header.put('x5u', url) + assertEquals url, header.get('x5u') + assertEquals uri, header.getX509Url() + } + + @Test + void testX509CertChain() { + def bundle = TestKeys.RS256 + List encodedCerts = Collections.of(Encoders.BASE64.encode(bundle.cert.getEncoded())) + header.setX509CertificateChain(bundle.chain) + assertEquals bundle.chain, header.getX509CertificateChain() + assertEquals encodedCerts, header.get('x5c') + } + + @Test + void testX509CertSha1Thumbprint() { + byte[] thumbprint = new byte[16] // simulate + Randoms.secureRandom().nextBytes(thumbprint) + String encoded = Encoders.BASE64URL.encode(thumbprint) + header.setX509CertificateSha1Thumbprint(thumbprint) + assertArrayEquals thumbprint, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testX509CertSha256Thumbprint() { + byte[] thumbprint = new byte[32] // simulate + Randoms.secureRandom().nextBytes(thumbprint) + String encoded = Encoders.BASE64URL.encode(thumbprint) + header.setX509CertificateSha256Thumbprint(thumbprint) + assertArrayEquals thumbprint, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testCritical() { + Set crits = Collections.setOf('foo', 'bar') + header.setCritical(crits) + assertEquals crits, header.getCritical() + } + + @Test + void testCritNull() { + header.put('crit', null) + assertNull header.getCritical() + } + + @Test + void testCritEmpty() { + header.put('crit', []) + assertNull header.getCritical() + } + + @Test + void testCritSingleValue() { + header.put('crit', 'foo') + assertEquals(["foo"] as Set, header.get('crit')) + assertEquals(["foo"] as Set, header.getCritical()) + } + + @Test + void testCritArray() { + String[] crit = ["exp"] as String[] + header.put('crit', crit) + assertEquals(["exp"] as Set, header.get('crit')) + assertEquals(["exp"] as Set, header.getCritical()) + } + + @Test + void testCritList() { + List crit = ["exp"] as List + header.put('crit', crit) + assertEquals(["exp"] as Set, header.get('crit')) + assertEquals(["exp"] as Set, header.getCritical()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index e5bc9b24..8a61b009 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -17,8 +17,11 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.Claims import io.jsonwebtoken.RequiredTypeException +import io.jsonwebtoken.impl.lang.Field +import io.jsonwebtoken.lang.DateFormats import org.junit.Before import org.junit.Test + import static org.junit.Assert.* class DefaultClaimsTest { @@ -152,20 +155,205 @@ class DefaultClaimsTest { } @Test - void testGetClaimWithRequiredType_Date_Success() { - def actual = new Date(); - claims.put("aDate", actual) - Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual) + void testGetRequiredIntegerFromLong() { + claims.put('foo', Long.valueOf(Integer.MAX_VALUE)) + assertEquals Integer.MAX_VALUE, claims.get('foo', Integer.class) as Integer } @Test - void testGetClaimWithRequiredType_DateWithLong_Success() { - def actual = new Date(); + void testGetRequiredIntegerWouldCauseOverflow() { + claims.put('foo', Long.MAX_VALUE) + try { + claims.get('foo', Integer.class) + } catch (RequiredTypeException expected) { + String msg = "Claim 'foo' value is too large or too small to be represented as a java.lang.Integer instance (would cause numeric overflow)." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testGetRequiredDateFromNull() { + Date date = claims.get("aDate", Date.class) + assertNull date + } + + @Test + void testGetRequiredDateFromDate() { + def expected = new Date() + claims.put("aDate", expected) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromCalendar() { + def c = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + def expected = c.getTime() + claims.put("aDate", c) + Date result = claims.get('aDate', Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromLong() { + def expected = new Date() // note that Long is stored in claim - claims.put("aDate", actual.getTime()) - Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual) + claims.put("aDate", expected.getTime()) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromIso8601String() { + def expected = new Date() + claims.put("aDate", DateFormats.formatIso8601(expected)) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromIso8601MillisString() { + def expected = new Date() + claims.put("aDate", DateFormats.formatIso8601(expected, true)) + Date result = claims.get("aDate", Date.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromInvalidIso8601String() { + Date d = new Date() + String s = d.toString() + claims.put('aDate', s) + try { + claims.get('aDate', Date.class) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = "Cannot create Date from 'aDate' value '$s'. Cause: " + + "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics " + + "exhausted. Cause: Unparseable date: \"$s\"" + assertEquals expectedMsg, expected.getMessage() + } + } + + @Test + void testToSpecDateWithNull() { + assertNull claims.get(Claims.EXPIRATION) + assertNull claims.getExpiration() + assertNull claims.get(Claims.ISSUED_AT) + assertNull claims.getIssuedAt() + assertNull claims.get(Claims.NOT_BEFORE) + assertNull claims.getNotBefore() + } + + @Test + void testGetSpecDateWithLongString() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + Date expected = new Date(seconds * 1000L) + String secondsString = '' + seconds + claims.put(Claims.EXPIRATION, secondsString) + claims.put(Claims.ISSUED_AT, secondsString) + claims.put(Claims.NOT_BEFORE, secondsString) + assertEquals expected, claims.getExpiration() + assertEquals expected, claims.getIssuedAt() + assertEquals expected, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithLong() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + Date expected = new Date(seconds * 1000L) + claims.put(Claims.EXPIRATION, seconds) + claims.put(Claims.ISSUED_AT, seconds) + claims.put(Claims.NOT_BEFORE, seconds) + assertEquals expected, claims.getExpiration() + assertEquals expected, claims.getIssuedAt() + assertEquals expected, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithIso8601String() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + String s = DateFormats.formatIso8601(orig) + claims.put(Claims.EXPIRATION, s) + claims.put(Claims.ISSUED_AT, s) + claims.put(Claims.NOT_BEFORE, s) + assertEquals orig, claims.getExpiration() + assertEquals orig, claims.getIssuedAt() + assertEquals orig, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithDate() { + Date orig = new Date() + long millis = orig.getTime() + long seconds = millis / 1000L as long + claims.put(Claims.EXPIRATION, orig) + claims.put(Claims.ISSUED_AT, orig) + claims.put(Claims.NOT_BEFORE, orig) + assertEquals orig, claims.getExpiration() + assertEquals orig, claims.getIssuedAt() + assertEquals orig, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testGetSpecDateWithCalendar() { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + Date date = cal.getTime() + long millis = date.getTime() + long seconds = millis / 1000L as long + claims.put(Claims.EXPIRATION, cal) + claims.put(Claims.ISSUED_AT, cal) + claims.put(Claims.NOT_BEFORE, cal) + assertEquals date, claims.getExpiration() + assertEquals date, claims.getIssuedAt() + assertEquals date, claims.getNotBefore() + assertEquals seconds, claims.get(Claims.EXPIRATION) + assertEquals seconds, claims.get(Claims.ISSUED_AT) + assertEquals seconds, claims.get(Claims.NOT_BEFORE) + } + + @Test + void testToSpecDateWithDate() { + long millis = System.currentTimeMillis() + Date d = new Date(millis) + claims.put('exp', d) + assertEquals d, claims.getExpiration() + } + + void trySpecDateNonDate(Field field) { + def val = new Object() { @Override String toString() {return 'hi'} } + try { + claims.put(field.getId(), val) + fail() + } catch (IllegalArgumentException iae) { + String msg = "Invalid JWT Claim $field value: hi. Cannot create Date from object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1." + assertEquals msg, iae.getMessage() + } + } + + @Test + void testSpecDateFromNonDateObject() { + trySpecDateNonDate(DefaultClaims.EXPIRATION) + trySpecDateNonDate(DefaultClaims.ISSUED_AT) + trySpecDateNonDate(DefaultClaims.NOT_BEFORE) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultDynamicHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultDynamicHeaderBuilderTest.groovy new file mode 100644 index 00000000..a4abc505 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultDynamicHeaderBuilderTest.groovy @@ -0,0 +1,253 @@ +/* + * 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.JweHeader +import io.jsonwebtoken.JwsHeader +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.UnprotectedHeader +import io.jsonwebtoken.impl.security.DefaultHashAlgorithm +import io.jsonwebtoken.impl.security.DefaultRequest +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.Request +import io.jsonwebtoken.security.StandardHashAlgorithms +import org.junit.Before +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +class DefaultDynamicHeaderBuilderTest { + + static DefaultDynamicHeaderBuilder builder + + @Before + void setUp() { + builder = Jwts.header() as DefaultDynamicHeaderBuilder + } + + @Test + void testType() { + String type = 'foo' + def header = builder.setType(type).build() + assertTrue header instanceof UnprotectedHeader + assertEquals type, header.getType() + } + + @Test + void testContentType() { + String cty = 'text/plain' + def header = builder.setContentType(cty).build() + assertTrue header instanceof UnprotectedHeader + assertEquals cty, header.getContentType() + } + + @Test + void testAlgorithm() { + String alg = 'none' + def header = builder.setAlgorithm(alg).build() + assertTrue header instanceof UnprotectedHeader + assertEquals alg, header.getAlgorithm() + } + + @Test + void testCompressionAlgorithm() { + String zip = 'DEF' + def header = builder.setCompressionAlgorithm(zip).build() + assertTrue header instanceof UnprotectedHeader + assertEquals zip, header.getCompressionAlgorithm() + } + + @Test + void testPut() { + def header = builder.put('foo', 'bar').build() + assertTrue header instanceof UnprotectedHeader + assertEquals 'bar', header.get('foo') + } + + @Test + void testPutAll() { + def m = ['foo': 'bar', 'baz': 'bat'] + def header = builder.putAll(m).build() + assertTrue header instanceof UnprotectedHeader + assertEquals m, header + } + + @Test + void testRemove() { + def header = builder.put('foo', 'bar').remove('foo').build() + assertTrue header instanceof UnprotectedHeader + assertTrue header.isEmpty() + } + + @Test + void testClear() { + def m = ['foo': 'bar', 'baz': 'bat'] + def header = builder.putAll(m).clear().build() + assertTrue header instanceof UnprotectedHeader + assertTrue header.isEmpty() + } + + @Test + void testSetJwkSetUrl() { + URI uri = URI.create('https://github.com/jwtk/jjwt') + def header = builder.setJwkSetUrl(uri).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertEquals uri, header.getJwkSetUrl() + } + + @Test + void testSetJwk() { + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() + def header = builder.setJwk(jwk).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertEquals jwk, header.getJwk() + } + + @Test + void testSetKeyId() { + def header = builder.setKeyId('kid').build() as JwsHeader + assertTrue header instanceof JwsHeader + assertEquals 'kid', header.getKeyId() + } + + @Test + void testSetX509Url() { + URI uri = URI.create('https://github.com/jwtk/jjwt') + def header = builder.setX509Url(uri).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertEquals uri, header.getX509Url() + } + + @Test + void testSetX509CertificateChain() { + def chain = TestKeys.RS256.chain + def header = builder.setX509CertificateChain(chain).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertEquals chain, header.getX509CertificateChain() + } + + @Test + void testSetX509CertificateSha1Thumbprint() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5t = DefaultHashAlgorithm.SHA1.digest(request) + String encoded = Encoders.BASE64URL.encode(x5t) + def header = builder.setX509CertificateSha1Thumbprint(x5t).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertArrayEquals x5t, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testSetX509CertificateSha1ThumbprintEnabled() { + def chain = TestKeys.RS256.chain + Request request = new DefaultRequest(chain[0].getEncoded(), null, null) + def x5t = DefaultHashAlgorithm.SHA1.digest(request) + String encoded = Encoders.BASE64URL.encode(x5t) + def header = builder.setX509CertificateChain(chain).withX509Sha1Thumbprint(true).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertArrayEquals x5t, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testSetX509CertificateSha256Thumbprint() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5tS256 = Jwks.HASH.SHA256.digest(request) + String encoded = Encoders.BASE64URL.encode(x5tS256) + def header = builder.setX509CertificateSha256Thumbprint(x5tS256).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertArrayEquals x5tS256, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testSetX509CertificateSha256ThumbprintEnabled() { + def chain = TestKeys.RS256.chain + Request request = new DefaultRequest(chain[0].getEncoded(), null, null) + def x5tS256 = StandardHashAlgorithms.get().SHA256.digest(request) + String encoded = Encoders.BASE64URL.encode(x5tS256) + def header = builder.setX509CertificateChain(chain).withX509Sha256Thumbprint(true).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertArrayEquals x5tS256, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testSetCritical() { + def crit = ['exp', 'sub'] as Set + def header = builder.setCritical(crit).build() as JwsHeader + assertTrue header instanceof JwsHeader + assertEquals crit, header.getCritical() + } + + @Test + void testSetAgreementPartyUInfo() { + def info = "UInfo".getBytes(StandardCharsets.UTF_8) + def header = builder.setAgreementPartyUInfo(info).build() as JweHeader + assertTrue header instanceof JweHeader + assertArrayEquals info, header.getAgreementPartyUInfo() + } + + @Test + void testSetAgreementPartyUInfoString() { + def s = "UInfo" + def info = s.getBytes(StandardCharsets.UTF_8) + def header = builder.setAgreementPartyUInfo(s).build() as JweHeader + assertTrue header instanceof JweHeader + assertArrayEquals info, header.getAgreementPartyUInfo() + } + + @Test + void testSetAgreementPartyVInfo() { + def info = "VInfo".getBytes(StandardCharsets.UTF_8) + def header = builder.setAgreementPartyVInfo(info).build() as JweHeader + assertTrue header instanceof JweHeader + assertArrayEquals info, header.getAgreementPartyVInfo() + } + + @Test + void testSetAgreementPartyVInfoString() { + def s = "VInfo" + def info = s.getBytes(StandardCharsets.UTF_8) + def header = builder.setAgreementPartyVInfo(s).build() as JweHeader + assertTrue header instanceof JweHeader + assertArrayEquals info, header.getAgreementPartyVInfo() + } + + @Test + void testSetPbes2Count() { + int count = 4096 + def header = builder.setPbes2Count(count).build() as JweHeader + assertTrue header instanceof JweHeader + assertEquals count, header.getPbes2Count() + } + + @Test + void testUnprotectedHeaderChangedToProtectedHeaderChangedToJweHeader() { + def header = builder.put('foo', 'bar') // all headers + .setKeyId('baz') // protected header properties + .setPbes2Count(2048).setAgreementPartyUInfo("info").build() as JweHeader // JWE properties + assertTrue header instanceof JweHeader + def encoded = Encoders.BASE64URL.encode('info'.getBytes(StandardCharsets.UTF_8)) + def expected = new DefaultJweHeader(['foo': 'bar', 'kid': 'baz', 'p2c': 2048, 'apu': encoded]) + assertEquals expected, header + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy deleted file mode 100644 index d10a756b..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2015 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.Header -import org.junit.Test -import static org.junit.Assert.* - -class DefaultHeaderTest { - - @Test - void testType() { - - def h = new DefaultHeader() - - h.setType('foo') - assertEquals h.getType(), 'foo' - } - - @Test - void testContentType() { - - def h = new DefaultHeader() - - h.setContentType('bar') - assertEquals h.getContentType(), 'bar' - } - - @Test - void testSetCompressionAlgorithm() { - def h = new DefaultHeader() - h.setCompressionAlgorithm("DEF") - assertEquals "DEF", h.getCompressionAlgorithm() - } - - @Test - void testBackwardsCompatibleCompressionHeader() { - def h = new DefaultHeader() - h.put(Header.DEPRECATED_COMPRESSION_ALGORITHM, "DEF") - assertEquals "DEF", h.getCompressionAlgorithm() - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderBuilderTest.groovy new file mode 100644 index 00000000..f65ce363 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderBuilderTest.groovy @@ -0,0 +1,70 @@ +/* + * 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 org.junit.Before +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +class DefaultJweHeaderBuilderTest { + + DefaultJweHeaderBuilder builder + + @Before + void testSetUp() { + builder = new DefaultJweHeaderBuilder() + } + + @Test + void testNewHeader() { + assertTrue builder.header instanceof DefaultJweHeader + } + + @Test + void testSetAgreementPartyUInfo() { + def info = "UInfo".getBytes(StandardCharsets.UTF_8) + assertArrayEquals info, builder.setAgreementPartyUInfo(info).build().getAgreementPartyUInfo() + } + + @Test + void testSetAgreementPartyUInfoString() { + def s = "UInfo" + def info = s.getBytes(StandardCharsets.UTF_8) + assertArrayEquals info, builder.setAgreementPartyUInfo(s).build().getAgreementPartyUInfo() + } + + @Test + void testSetAgreementPartyVInfo() { + def info = "VInfo".getBytes(StandardCharsets.UTF_8) + assertArrayEquals info, builder.setAgreementPartyVInfo(info).build().getAgreementPartyVInfo() + } + + @Test + void testSetAgreementPartyVInfoString() { + def s = "VInfo" + def info = s.getBytes(StandardCharsets.UTF_8) + assertArrayEquals info, builder.setAgreementPartyVInfo(s).build().getAgreementPartyVInfo() + } + + @Test + void testSetPbes2Count() { + int count = 4096 + assertEquals count, builder.setPbes2Count(count).build().getPbes2Count() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy new file mode 100644 index 00000000..c6676b1f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2018 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.impl.security.Randoms +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPublicJwk +import org.junit.Before +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.PublicKey +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey +import java.util.concurrent.atomic.AtomicInteger + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class DefaultJweHeaderTest { + + private DefaultJweHeader header + + @Before + void setUp() { + header = new DefaultJweHeader() + } + + @Test + void testEncryptionAlgorithm() { + header.put('enc', 'foo') + assertEquals 'foo', header.getEncryptionAlgorithm() + + header = new DefaultJweHeader([enc: 'bar']) + assertEquals 'bar', header.getEncryptionAlgorithm() + } + + @Test + void testGetName() { + assertEquals 'JWE header', header.getName() + } + + @Test + void testEpkWithSecretJwk() { + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=oct, k=}. " + + "Value must be a Public JWK, not a Secret JWK." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEpkWithPrivateJwk() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.private as ECPrivateKey).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=EC, crv=P-256, " + + "x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, " + + "d=}. Value must be a Public JWK, not an EC Private JWK." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEpkWithRsaPublicJwk() { + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + header.put('epk', values) + def epk = header.getEphemeralPublicKey() + assertTrue epk instanceof RsaPublicJwk + assertEquals(jwk, epk) + } + + @Test + void testEpkWithEcPublicJwkValues() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def values = new LinkedHashMap(jwk) //extract values to remove JWK type + header.put('epk', values) + assertEquals jwk, header.get('epk') + } + + @Test + void testEpkWithInvalidEcPublicJwk() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() + def values = new LinkedHashMap(jwk) // copy fields so we can mutate + // We have a public JWK for a point on the curve, now swap out the x coordinate for something invalid: + values.put('x', 'Kg') + try { + header.put('epk', values) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=EC, crv=P-256, x=Kg, " + + "y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk}. EC JWK x,y coordinates do not exist on " + + "elliptic curve 'P-256'. This could be due simply to an incorrectly-created JWK or possibly an " + + "attempted Invalid Curve Attack (see https://safecurves.cr.yp.to/twist.html for more " + + "information)." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEpkWithEcPublicJwk() { + def jwk = Jwks.builder().forKey(TestKeys.ES256.pair.public as ECPublicKey).build() + header.put('epk', jwk) + assertEquals jwk, header.get('epk') + assertEquals jwk, header.getEphemeralPublicKey() + } + + @Test + void testEpkWithEdPublicJwk() { + def keys = TestKeys.EdEC.collect({it -> it.pair.public as PublicKey}) + for(PublicKey key : keys) { + def jwk = Jwks.builder().forKey((PublicKey)key as PublicKey).build() + header.put('epk', jwk) + assertEquals jwk, header.get('epk') + assertEquals jwk, header.getEphemeralPublicKey() + } + } + + @Test + void testAgreementPartyUInfo() { + String val = "Party UInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyUInfo(info) + assertArrayEquals info, header.getAgreementPartyUInfo() + } + + @Test + void testAgreementPartyUInfoString() { + String val = "Party UInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyUInfo(val) + assertArrayEquals info, header.getAgreementPartyUInfo() + } + + @Test + void testEmptyAgreementPartyUInfo() { + byte[] info = new byte[0] + header.setAgreementPartyUInfo(info) + assertNull header.getAgreementPartyUInfo() + } + + @Test + void testEmptyAgreementPartyUInfoString() { + String s = ' ' + header.setAgreementPartyUInfo(s) + assertNull header.getAgreementPartyUInfo() + } + + @Test + void testAgreementPartyVInfo() { + String val = "Party VInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyVInfo(info) + assertArrayEquals info, header.getAgreementPartyVInfo() + } + + @Test + void testAgreementPartyVInfoString() { + String val = "Party VInfo" + byte[] info = val.getBytes(StandardCharsets.UTF_8) + header.setAgreementPartyVInfo(val) + assertArrayEquals info, header.getAgreementPartyVInfo() + } + + @Test + void testEmptyAgreementPartyVInfo() { + byte[] info = new byte[0] + header.setAgreementPartyVInfo(info) + assertNull header.getAgreementPartyVInfo() + } + + @Test + void testEmptyAgreementPartyVInfoString() { + String s = ' ' + header.setAgreementPartyVInfo(s) + assertNull header.getAgreementPartyVInfo() + } + + @Test + void testIv() { + byte[] bytes = new byte[12] + Randoms.secureRandom().nextBytes(bytes) + header.put('iv', bytes) + assertEquals Encoders.BASE64URL.encode(bytes), header.get('iv') + assertTrue MessageDigest.isEqual(bytes, header.getInitializationVector()) + } + + @Test + void testIvWithIncorrectSize() { + byte[] bytes = new byte[7] + Randoms.secureRandom().nextBytes(bytes) + try { + header.put('iv', bytes) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'iv' (Initialization Vector) value. " + + "Byte array must be exactly 96 bits (12 bytes). Found 56 bits (7 bytes)" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testTag() { + byte[] bytes = new byte[16] + Randoms.secureRandom().nextBytes(bytes) + header.put('tag', bytes) + assertEquals Encoders.BASE64URL.encode(bytes), header.get('tag') + assertTrue MessageDigest.isEqual(bytes, header.getAuthenticationTag()) + } + + @Test + void testTagWithIncorrectSize() { + byte[] bytes = new byte[15] + Randoms.secureRandom().nextBytes(bytes) + try { + header.put('tag', bytes) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'tag' (Authentication Tag) value. " + + "Byte array must be exactly 128 bits (16 bytes). Found 120 bits (15 bytes)" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cByte() { + header.put('p2c', Byte.MAX_VALUE) + assertEquals 127, header.getPbes2Count() + } + + @Test + void testP2cShort() { + header.put('p2c', Short.MAX_VALUE) + assertEquals 32767, header.getPbes2Count() + } + + @Test + void testP2cInt() { + header.put('p2c', Integer.MAX_VALUE) + assertEquals 0x7fffffff as Integer, header.getPbes2Count() + } + + @Test + void testP2cAtomicInteger() { + header.put('p2c', new AtomicInteger(Integer.MAX_VALUE)) + assertEquals 0x7fffffff as Integer, header.getPbes2Count() + } + + @Test + void testP2cString() { + header.put('p2c', "100") + assertEquals 100, header.getPbes2Count() + } + + @Test + void testP2cZero() { + try { + header.put('p2c', 0) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 0. Value must be a positive integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cNegative() { + try { + header.put('p2c', -1) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: -1. Value must be a positive integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cTooLarge() { + try { + header.put('p2c', Long.MAX_VALUE) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: 9223372036854775807. " + + "Value cannot be represented as a java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testP2cDecimal() { + double d = 42.2348423d + try { + header.put('p2c', d) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2c' (PBES2 Count) value: $d. " + + "Value cannot be represented as a java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testPbe2SaltBytes() { + byte[] salt = new byte[32] + Randoms.secureRandom().nextBytes(salt) + header.put('p2s', salt) + assertEquals Encoders.BASE64URL.encode(salt), header.get('p2s') + assertArrayEquals salt, header.getPbes2Salt() + } + + @Test + void pbe2SaltStringTest() { + byte[] salt = new byte[32] + Randoms.secureRandom().nextBytes(salt) + String val = Encoders.BASE64URL.encode(salt) + header.put('p2s', val) + //ensure that even though a Base64Url string was set, we get back a byte[]: + assertArrayEquals salt, header.getPbes2Salt() + } + + @Test + void testPbe2SaltInputTooSmall() { + byte[] salt = new byte[7] // RFC requires a minimum of 64 bits (8 bytes), so we go 1 byte less + Randoms.secureRandom().nextBytes(salt) + String val = Encoders.BASE64URL.encode(salt) + try { + header.put('p2s', val) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Invalid JWE header 'p2s' (PBES2 Salt Input) value: $val. " + + "Byte array must be at least 64 bits (8 bytes). Found 56 bits (7 bytes)" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy new file mode 100644 index 00000000..ba9853a8 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweTest.groovy @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 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.Jwts +import io.jsonwebtoken.io.Encoders +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotEquals + +class DefaultJweTest { + + @Test + void testToString() { + def alg = Jwts.ENC.A128CBC_HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.builder().claim('foo', 'bar').encryptWith(key, alg).compact() + def jwe = Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(compact) + String encodedIv = Encoders.BASE64URL.encode(jwe.initializationVector) + String encodedTag = Encoders.BASE64URL.encode(jwe.aadTag) + String expected = "header={alg=dir, enc=A128CBC-HS256},payload={foo=bar},iv=$encodedIv,tag=$encodedTag" + assertEquals expected, jwe.toString() + } + + @Test + void testEqualsAndHashCode() { + def alg = Jwts.ENC.A128CBC_HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.builder().claim('foo', 'bar').encryptWith(key, alg).compact() + def parser = Jwts.parserBuilder().decryptWith(key).build() + def jwe1 = parser.parseClaimsJwe(compact) + def jwe2 = parser.parseClaimsJwe(compact) + assertNotEquals jwe1, 'hello' as String + assertEquals jwe1, jwe1 + assertEquals jwe2, jwe2 + assertEquals jwe1, jwe2 + assertEquals jwe1.hashCode(), jwe2.hashCode() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderBuilderTest.groovy new file mode 100644 index 00000000..aaea0be4 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderBuilderTest.groovy @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 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.impl.security.DefaultHashAlgorithm +import io.jsonwebtoken.impl.security.DefaultRequest +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.Request +import io.jsonwebtoken.security.StandardHashAlgorithms +import org.junit.Before +import org.junit.Test + +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +class DefaultJwsHeaderBuilderTest { + + DefaultJwsHeaderBuilder builder + + @Before + void testSetUp() { + builder = new DefaultJwsHeaderBuilder() + } + + @Test + void testNewHeader() { + assertTrue builder.header instanceof DefaultJwsHeader + } + + @Test + void testSetJwkSetUrl() { + URI uri = URI.create('https://github.com/jwtk/jjwt') + assertEquals uri, builder.setJwkSetUrl(uri).build().getJwkSetUrl() + } + + @Test + void testSetJwk() { + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() + assertEquals jwk, builder.setJwk(jwk).build().getJwk() + } + + @Test + void testSetKeyId() { + assertEquals 'kid', builder.setKeyId('kid').build().getKeyId() + } + + @Test + void testSetX509Url() { + URI uri = URI.create('https://github.com/jwtk/jjwt') + assertEquals uri, builder.setX509Url(uri).build().getX509Url() + } + + @Test + void testSetX509CertificateChain() { + def chain = TestKeys.RS256.chain + assertEquals chain, builder.setX509CertificateChain(chain).build().getX509CertificateChain() + } + + @Test + void testSetX509CertificateSha1Thumbprint() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5t = DefaultHashAlgorithm.SHA1.digest(request) + String encoded = Encoders.BASE64URL.encode(x5t) + def header = builder.setX509CertificateSha1Thumbprint(x5t).build() + assertArrayEquals x5t, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testSetX509CertificateSha1ThumbprintEnabled() { + def chain = TestKeys.RS256.chain + Request request = new DefaultRequest(chain[0].getEncoded(), null, null) + def x5t = DefaultHashAlgorithm.SHA1.digest(request) + String encoded = Encoders.BASE64URL.encode(x5t) + def header = builder.setX509CertificateChain(chain).withX509Sha1Thumbprint(true).build() + assertArrayEquals x5t, header.getX509CertificateSha1Thumbprint() + assertEquals encoded, header.get('x5t') + } + + @Test + void testSetX509CertificateSha256Thumbprint() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5tS256 = StandardHashAlgorithms.get().SHA256.digest(request) + String encoded = Encoders.BASE64URL.encode(x5tS256) + def header = builder.setX509CertificateSha256Thumbprint(x5tS256).build() + assertArrayEquals x5tS256, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testSetX509CertificateSha256ThumbprintEnabled() { + def chain = TestKeys.RS256.chain + Request request = new DefaultRequest(chain[0].getEncoded(), null, null) + def x5tS256 = StandardHashAlgorithms.get().SHA256.digest(request) + String encoded = Encoders.BASE64URL.encode(x5tS256) + def header = builder.setX509CertificateChain(chain).withX509Sha256Thumbprint(true).build() + assertArrayEquals x5tS256, header.getX509CertificateSha256Thumbprint() + assertEquals encoded, header.get('x5t#S256') + } + + @Test + void testSetCritical() { + def crit = ['exp', 'sub'] as Set + assertEquals crit, builder.setCritical(crit).build().getCritical() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy index 9d43dfd9..6e28ef6e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy @@ -16,16 +16,14 @@ package io.jsonwebtoken.impl import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals class DefaultJwsHeaderTest { @Test - void testKeyId() { - - def h = new DefaultJwsHeader() - - h.setKeyId('foo') - assertEquals h.getKeyId(), 'foo' + void testGetName() { + def header = new DefaultJwsHeader() + assertEquals 'JWS header', header.getName() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy index b2fee93e..2661e2d6 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy @@ -17,35 +17,45 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys import org.junit.Test -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import static org.junit.Assert.* class DefaultJwsTest { @Test void testConstructor() { - - JwsHeader header = Jwts.jwsHeader() + JwsHeader header = new DefaultJwsHeader() def jws = new DefaultJws(header, 'foo', 'sig') - assertSame jws.getHeader(), header - assertEquals jws.getBody(), 'foo' + assertEquals jws.getPayload(), 'foo' assertEquals jws.getSignature(), 'sig' } @Test void testToString() { //create random signing key for testing: - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - byte[] key = Keys.secretKeyFor(alg).encoded - String compact = Jwts.builder().claim('foo', 'bar').signWith(alg, key).compact(); + def alg = Jwts.SIG.HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact() int i = compact.lastIndexOf('.') String signature = compact.substring(i + 1) - def jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compact) - assertEquals 'header={alg=HS256},body={foo=bar},signature=' + signature, jws.toString() + def jws = Jwts.parserBuilder().verifyWith(key).build().parseClaimsJws(compact) + assertEquals 'header={alg=HS256},payload={foo=bar},signature=' + signature, jws.toString() + } + + @Test + void testEqualsAndHashCode() { + def alg = Jwts.SIG.HS256 + def key = alg.keyBuilder().build() + String compact = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact() + def parser = Jwts.parserBuilder().verifyWith(key).build() + def jws1 = parser.parseClaimsJws(compact) + def jws2 = parser.parseClaimsJws(compact) + assertNotEquals jws1, 'hello' as String + assertEquals jws1, jws1 + assertEquals jws2, jws2 + assertEquals jws1, jws2 + assertEquals jws1.hashCode(), jws2.hashCode() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 4c998423..888b8247 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -19,29 +19,123 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.CompressionCodecs import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Encoder -import io.jsonwebtoken.io.EncodingException -import io.jsonwebtoken.io.SerializationException -import io.jsonwebtoken.io.Serializer -import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.impl.security.Randoms +import io.jsonwebtoken.impl.security.TestKeys +import io.jsonwebtoken.io.* +import io.jsonwebtoken.security.* +import org.junit.Before import org.junit.Test import javax.crypto.KeyGenerator -import javax.crypto.SecretKeyFactory -import java.security.KeyFactory +import java.nio.charset.StandardCharsets +import java.security.Provider +import java.security.SecureRandom +import static org.easymock.EasyMock.* import static org.junit.Assert.* class DefaultJwtBuilderTest { - private static ObjectMapper objectMapper = new ObjectMapper(); + private static ObjectMapper objectMapper = new ObjectMapper() + + private DefaultJwtBuilder builder + + @Before + void setUp() { + this.builder = new DefaultJwtBuilder() + } + + @Test + void testSetProvider() { + + Provider provider = createMock(Provider) + + final boolean[] called = new boolean[1] + + io.jsonwebtoken.security.SignatureAlgorithm alg = new io.jsonwebtoken.security.SignatureAlgorithm() { + @Override + byte[] digest(SecureRequest request) throws SignatureException, KeyException { + assertSame provider, request.getProvider() + called[0] = true + //simulate a digest: + byte[] bytes = new byte[32] + Randoms.secureRandom().nextBytes(bytes) + return bytes + } + + @Override + boolean verify(VerifySecureDigestRequest request) throws SignatureException, KeyException { + throw new IllegalStateException("should not be called during build") + } + + @Override + KeyPairBuilder keyPairBuilder() { + throw new IllegalStateException("should not be called during build") + } + + @Override + String getId() { + return "test" + } + } + + replay provider + def b = new DefaultJwtBuilder().setProvider(provider) + .setSubject('me').signWith(Jwts.SIG.HS256.keyBuilder().build(), alg) + assertSame provider, b.provider + b.compact() + verify provider + assertTrue called[0] + } + + @Test + void testSetSecureRandom() { + + final SecureRandom random = new SecureRandom() + + final boolean[] called = new boolean[1] + + io.jsonwebtoken.security.SignatureAlgorithm alg = new io.jsonwebtoken.security.SignatureAlgorithm() { + @Override + byte[] digest(SecureRequest request) throws SignatureException, KeyException { + assertSame random, request.getSecureRandom() + called[0] = true + //simulate a digest: + byte[] bytes = new byte[32] + Randoms.secureRandom().nextBytes(bytes) + return bytes + } + + @Override + boolean verify(VerifySecureDigestRequest request) throws SignatureException, KeyException { + throw new IllegalStateException("should not be called during build") + } + + @Override + KeyPairBuilder keyPairBuilder() { + throw new IllegalStateException("should not be called during build") + } + + @Override + String getId() { + return "test" + } + } + + def b = new DefaultJwtBuilder().setSecureRandom(random) + .setSubject('me').signWith(Jwts.SIG.HS256.keyBuilder().build(), alg) + assertSame random, b.secureRandom + b.compact() + assertTrue called[0] + } @Test void testSetHeader() { - def h = Jwts.header() + def h = Jwts.unprotectedHeader() def b = new DefaultJwtBuilder() b.setHeader(h) - assertSame b.header, h + assertEquals h, b.header } @Test @@ -112,6 +206,13 @@ class DefaultJwtBuilderTest { assertEquals b.claims.foo, 'bar' } + @Test + void testClaimEmptyString() { + String value = ' ' + builder.claim('foo', value) + assertNull builder.claims // shouldn't auto-create claims instance + } + @Test void testExistingClaimsAndSetClaim() { def b = new DefaultJwtBuilder() @@ -140,76 +241,78 @@ class DefaultJwtBuilderTest { @Test void testCompactWithoutPayloadOrClaims() { - def compact = new DefaultJwtBuilder().compact() + def serializer = Services.loadFirst(Serializer.class) + def header = Encoders.BASE64URL.encode(serializer.serialize(['alg': 'none'])) + assertEquals "$header.." as String, new DefaultJwtBuilder().compact() + } - assertTrue compact.endsWith("..") + @Test + void testNullPayloadString() { + String payload = null + def serializer = Services.loadFirst(Serializer.class) + def header = Encoders.BASE64URL.encode(serializer.serialize(['alg': 'none'])) + assertEquals "$header.." as String, builder.setPayload((String) payload).compact() } @Test void testCompactWithBothPayloadAndClaims() { - def b = new DefaultJwtBuilder() - b.setPayload('foo') - b.claim('a', 'b') try { - b.compact() + builder.setPayload('foo').claim('a', 'b').compact() fail() } catch (IllegalStateException ise) { - assertEquals ise.message, "Both 'payload' and 'claims' cannot both be specified. Choose either one." + assertEquals ise.message, "Both 'content' and 'claims' cannot both be specified. Choose either one." } } @Test void testCompactWithJwsHeader() { def b = new DefaultJwtBuilder() - b.setHeader(Jwts.jwsHeader().setKeyId('a')) + b.setHeader(Jwts.header().setKeyId('a')) b.setPayload('foo') def alg = SignatureAlgorithm.HS256 def key = Keys.secretKeyFor(alg) b.signWith(key, alg) String s1 = b.compact() - //ensure deprecated signWith(alg, key) produces the same result: + //ensure deprecated with(alg, key) produces the same result: b.signWith(alg, key) String s2 = b.compact() assertEquals s1, s2 } @Test - void testBase64UrlEncodeError() { - - def b = new DefaultJwtBuilder() { + void testHeaderSerializationErrorException() { + def serializer = new Serializer() { @Override - protected byte[] toJson(Object o) throws SerializationException { + byte[] serialize(Object o) throws SerializationException { throw new SerializationException('foo', new Exception()) } } - + def b = new DefaultJwtBuilder().serializeToJsonWith(serializer) try { b.setPayload('foo').compact() fail() - } catch (IllegalStateException ise) { - assertEquals ise.cause.message, 'foo' + } catch (SerializationException expected) { + assertEquals 'Unable to serialize header to JSON. Cause: foo', expected.getMessage() } } @Test void testCompactCompressionCodecJsonProcessingException() { - def b = new DefaultJwtBuilder() { + def serializer = new Serializer() { @Override - protected byte[] toJson(Object o) throws SerializationException { - if (o instanceof DefaultJwsHeader) { - return super.toJson(o) - } + byte[] serialize(Object o) throws SerializationException { throw new SerializationException('dummy text', new Exception()) } } - - def c = Jwts.claims().setSubject("Joe"); - + def b = new DefaultJwtBuilder() + .setSubject("Joe") // ensures claims instance + .compressWith(CompressionCodecs.DEFLATE) + .serializeToJsonWith(serializer) try { - b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact() + b.compact() fail() - } catch (IllegalArgumentException iae) { - assertEquals iae.message, 'Unable to serialize claims object to json: dummy text' + } catch (SerializationException expected) { + assertEquals 'Unable to serialize claims to JSON. Cause: dummy text', expected.message } } @@ -217,7 +320,7 @@ class DefaultJwtBuilderTest { void testSignWithKeyOnly() { def b = new DefaultJwtBuilder() - b.setHeader(Jwts.jwsHeader().setKeyId('a')) + b.setHeader(Jwts.header().setKeyId('a')) b.setPayload('foo') def key = KeyGenerator.getInstance('HmacSHA256').generateKey() @@ -364,7 +467,72 @@ class DefaultJwtBuilderTest { .claim('foo', 'bar') .compact() - assertEquals 'bar', Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws).getBody().get('foo') + assertEquals 'bar', Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws).getPayload().get('foo') } + @Test + void testSignWithNoneAlgorithm() { + def key = TestKeys.HS256 + try { + builder.signWith(key, Jwts.SIG.NONE) + fail() + } catch (IllegalArgumentException expected) { + String msg = "The 'none' JWS algorithm cannot be used to sign JWTs." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testSignWithPublicKey() { + def key = TestKeys.RS256.pair.public + def alg = Jwts.SIG.RS256 + try { + builder.signWith(key, alg) + fail() + } catch (IllegalArgumentException iae) { + assertEquals(DefaultJwtBuilder.PUB_KEY_SIGN_MSG, iae.getMessage()) + } + } + + @Test + void testCompactSimplestPayload() { + def enc = Jwts.ENC.A128GCM + def key = enc.keyBuilder().build() + def jwe = builder.setPayload("me").encryptWith(key, enc).compact() + def jwt = Jwts.parserBuilder().decryptWith(key).build().parseContentJwe(jwe) + assertEquals 'me', new String(jwt.getPayload(), StandardCharsets.UTF_8) + } + + @Test + void testCompactSimplestClaims() { + def enc = Jwts.ENC.A128GCM + def key = enc.keyBuilder().build() + def jwe = builder.setSubject('joe').encryptWith(key, enc).compact() + def jwt = Jwts.parserBuilder().decryptWith(key).build().parseClaimsJwe(jwe) + assertEquals 'joe', jwt.getPayload().getSubject() + } + + @Test + void testSignWithAndEncryptWith() { + def key = TestKeys.HS256 + try { + builder.signWith(key).encryptWith(key, Jwts.ENC.A128GCM).compact() + fail() + } catch (IllegalStateException expected) { + String msg = "Both 'signWith' and 'encryptWith' cannot be specified - choose either." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEmptyPayloadAndClaimsJwe() { + def key = TestKeys.HS256 + try { + builder.encryptWith(key, Jwts.ENC.A128GCM).compact() + fail() + } catch (IllegalStateException expected) { + String msg = "Encrypted JWTs must have either 'claims' or non-empty 'content'." + assertEquals msg, expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 0ba131d1..6f55eeea 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -16,33 +16,65 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper -import io.jsonwebtoken.JwtParser -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.* +import io.jsonwebtoken.impl.security.ConstantKeyLocator +import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.Decoder import io.jsonwebtoken.io.DecodingException import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Deserializer -import io.jsonwebtoken.security.Keys import org.hamcrest.CoreMatchers +import org.junit.Before import org.junit.Test -import static org.easymock.EasyMock.niceMock -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame +import java.security.Provider + +import static org.easymock.EasyMock.* import static org.hamcrest.MatcherAssert.assertThat +import static org.junit.Assert.* // NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParserBuilder // implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class // just fills in any remaining test gaps. - class DefaultJwtParserBuilderTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + private DefaultJwtParserBuilder builder + + @Before + void setUp() { + builder = new DefaultJwtParserBuilder() + } + + @Test + void testSetProvider() { + Provider provider = createMock(Provider) + replay provider + + def parser = builder.setProvider(provider).build() + + assertSame provider, parser.jwtParser.provider + verify provider + } + + @Test + void testKeyLocatorAndDecryptionKeyConfigured() { + try { + builder + .setKeyLocator(new ConstantKeyLocator(null, null)) + .decryptWith(TestKeys.A128GCM) + .build() + fail() + } catch (IllegalStateException e) { + String msg = "Both 'keyLocator' and 'decryptWith' key cannot be configured. Prefer 'keyLocator' if possible." + assertEquals msg, e.getMessage() + } + } + @Test(expected = IllegalArgumentException) void testBase64UrlDecodeWithNullArgument() { - new DefaultJwtParserBuilder().base64UrlDecodeWith(null) + builder.base64UrlDecodeWith(null) } @Test @@ -53,37 +85,38 @@ class DefaultJwtParserBuilderTest { return null } } - def b = new DefaultJwtParserBuilder().base64UrlDecodeWith(decoder) + def b = builder.base64UrlDecodeWith(decoder) assertSame decoder, b.base64UrlDecoder } @Test(expected = IllegalArgumentException) void testDeserializeJsonWithNullArgument() { - new DefaultJwtParserBuilder().deserializeJsonWith(null) + builder.deserializeJsonWith(null) } @Test - void testDesrializeJsonWithCustomSerializer() { + void testDeserializeJsonWithCustomSerializer() { def deserializer = new Deserializer() { @Override Object deserialize(byte[] bytes) throws DeserializationException { return OBJECT_MAPPER.readValue(bytes, Map.class) } } - def p = new DefaultJwtParserBuilder().deserializeJsonWith(deserializer) + def p = builder.deserializeJsonWith(deserializer) assertSame deserializer, p.deserializer - def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) + def alg = Jwts.SIG.HS256 + def key = alg.keyBuilder().build() - String jws = Jwts.builder().claim('foo', 'bar').signWith(key, SignatureAlgorithm.HS256).compact() + String jws = Jwts.builder().claim('foo', 'bar').signWith(key, alg).compact() - assertEquals 'bar', p.setSigningKey(key).build().parseClaimsJws(jws).getBody().get('foo') + assertEquals 'bar', p.verifyWith(key).build().parseClaimsJws(jws).getPayload().get('foo') } @Test void testMaxAllowedClockSkewSeconds() { long max = Long.MAX_VALUE / 1000 as long - new DefaultJwtParserBuilder().setAllowedClockSkewSeconds(max) // no exception should be thrown + builder.setAllowedClockSkewSeconds(max) // no exception should be thrown } @Test @@ -91,15 +124,85 @@ class DefaultJwtParserBuilderTest { long value = Long.MAX_VALUE / 1000 as long value = value + 1L try { - new DefaultJwtParserBuilder().setAllowedClockSkewSeconds(value) + builder.setAllowedClockSkewSeconds(value) } catch (IllegalArgumentException expected) { assertEquals DefaultJwtParserBuilder.MAX_CLOCK_SKEW_ILLEGAL_MSG, expected.message } } + @Test + void testCompressionCodecLocator() { + Locator locator = new Locator() { + @Override + CompressionCodec locate(Header header) { + return null + } + } + def parser = builder.setCompressionCodecLocator(locator).build() + assertSame locator, parser.jwtParser.compressionCodecLocator + } + + @Test + void testAddCompressionCodecs() { + def codec = new TestCompressionCodec(id: 'test') + def parser = builder.addCompressionCodecs([codec] as Set).build() + def header = Jwts.unprotectedHeader().setCompressionAlgorithm(codec.getId()) + assertSame codec, parser.jwtParser.compressionCodecLocator.locate(header) + } + + @Test + void testCompressionCodecLocatorAndExtraCompressionCodecs() { + def codec = new TestCompressionCodec(id: 'test') + Locator locator = new Locator() { + @Override + CompressionCodec locate(Header header) { + return null + } + } + try { + builder.setCompressionCodecLocator(locator).addCompressionCodecs([codec] as Set).build() + fail() + } catch (IllegalStateException expected) { + String msg = "Both 'addCompressionCodecs' and 'compressionCodecLocator' (or 'compressionCodecResolver') cannot be specified. Choose either." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEnableUnsecuredDecompressionWithoutEnablingUnsecuredJws() { + try { + builder.enableUnsecuredDecompression().build() + fail() + } catch (IllegalStateException ise) { + String expected = "'enableUnsecuredDecompression' is only relevant if 'enableUnsecuredJws' " + "is also configured. Please read the JavaDoc of both features before enabling either " + "due to their security implications." + assertEquals expected, ise.getMessage() + } + } + + @Test + void testDecompressUnprotectedJwtDefault() { + def codec = CompressionCodecs.GZIP + String jwt = Jwts.builder().compressWith(codec).setSubject('joe').compact() + try { + builder.enableUnsecuredJws().build().parse(jwt) + fail() + } catch (UnsupportedJwtException e) { + String expected = String.format(DefaultJwtParser.UNPROTECTED_DECOMPRESSION_MSG, codec.getId()) + assertEquals(expected, e.getMessage()) + } + } + + @Test + void testDecompressUnprotectedJwtEnabled() { + def codec = CompressionCodecs.GZIP + String jws = Jwts.builder().compressWith(codec).setSubject('joe').compact() + def jwt = builder.enableUnsecuredJws().enableUnsecuredDecompression().build().parse(jws) + assertEquals 'joe', ((Claims) jwt.getPayload()).getSubject() + } + @Test void testDefaultDeserializer() { - JwtParser parser = new DefaultJwtParserBuilder().build() + JwtParser parser = builder.build() assertThat parser.jwtParser.deserializer, CoreMatchers.instanceOf(JwtDeserializer) // TODO: When the ImmutableJwtParser replaces the default implementation this test will need updating, something like: @@ -109,12 +212,48 @@ class DefaultJwtParserBuilderTest { @Test void testUserSetDeserializerWrapped() { Deserializer deserializer = niceMock(Deserializer) - JwtParser parser = new DefaultJwtParserBuilder() - .deserializeJsonWith(deserializer) - .build() + JwtParser parser = builder.deserializeJsonWith(deserializer).build() // TODO: When the ImmutableJwtParser replaces the default implementation this test will need updating assertThat parser.jwtParser.deserializer, CoreMatchers.instanceOf(JwtDeserializer) assertSame deserializer, parser.jwtParser.deserializer.deserializer } + + @Test + void testVerificationKeyAndSigningKeyResolverBothConfigured() { + def key = TestKeys.HS256 + builder.verifyWith(key).setSigningKeyResolver(new ConstantKeyLocator(key, null)) + try { + builder.build() + fail() + } catch (IllegalStateException expected) { + String msg = "Both 'signingKeyResolver and 'verifyWith/signWith' key cannot be configured. " + "Choose either, or prefer `keyLocator` when possible." + assertEquals(msg, expected.getMessage()) + } + } + + static class TestCompressionCodec implements CompressionCodec { + + String id + + @Override + String getAlgorithmName() { + return this.id + } + + @Override + byte[] compress(byte[] content) throws CompressionException { + return new byte[0] + } + + @Override + byte[] decompress(byte[] compressed) throws CompressionException { + return new byte[0] + } + + @Override + String getId() { + return this.id + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index cad6d3d8..2dd71c25 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.Jwts import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.io.* import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.security.Keys @@ -26,10 +27,9 @@ import org.junit.Test import javax.crypto.Mac import javax.crypto.SecretKey +import java.nio.charset.StandardCharsets -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame -import static org.junit.Assert.assertTrue +import static org.junit.Assert.* // NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParser // implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class @@ -37,6 +37,9 @@ import static org.junit.Assert.assertTrue class DefaultJwtParserTest { + // all whitespace chars as defined by Character.isWhitespace: + static final String WHITESPACE_STR = ' \u0020 \u2028 \u2029 \t \n \u000B \f \r \u001C \u001D \u001E \u001F ' + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Test(expected = IllegalArgumentException) @@ -45,7 +48,7 @@ class DefaultJwtParserTest { } @Test - void testBase64UrlEncodeWithCustomDecoder() { + void testBase64UrlDecodeWithCustomDecoder() { def decoder = new Decoder() { @Override Object decode(Object o) throws DecodingException { @@ -56,6 +59,11 @@ class DefaultJwtParserTest { assertSame decoder, b.base64UrlDecoder } + @Test(expected = MalformedJwtException) + void testBase64UrlDecodeWithInvalidInput() { + new DefaultJwtParser().base64UrlDecode('20:SLDKJF;3993;----', 'test') + } + @Test(expected = IllegalArgumentException) void testDeserializeJsonWithNullArgument() { new DefaultJwtParser().deserializeJsonWith(null) @@ -77,7 +85,7 @@ class DefaultJwtParserTest { String jws = Jwts.builder().claim('foo', 'bar').signWith(key, SignatureAlgorithm.HS256).compact() - assertEquals 'bar', p.setSigningKey(key).parseClaimsJws(jws).getBody().get('foo') + assertEquals 'bar', p.setSigningKey(key).parseClaimsJws(jws).getPayload().get('foo') } @Test(expected = MalformedJwtException) @@ -150,4 +158,55 @@ class DefaultJwtParserTest { assertEquals DefaultJwtParserBuilder.MAX_CLOCK_SKEW_ILLEGAL_MSG, expected.message } } + + @Test + void testIsLikelyJsonWithEmptyString() { + assertFalse DefaultJwtParser.isLikelyJson(''.getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonWithEmptyBytes() { + assertFalse DefaultJwtParser.isLikelyJson(Bytes.EMPTY) + } + + @Test + void testIsLikelyJsonWithWhitespaceString() { + assertFalse DefaultJwtParser.isLikelyJson(WHITESPACE_STR.getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonWithOnlyOpeningBracket() { + assertFalse DefaultJwtParser.isLikelyJson(' {... '.getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonWithOnlyClosingBracket() { + assertFalse DefaultJwtParser.isLikelyJson(' } '.getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonMinimalJsonObject() { + assertTrue DefaultJwtParser.isLikelyJson("{}".getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonWithLeadingAndTrailingWhitespace() { + // all whitespace chars as defined by Character.isWhitespace: + String claimsJson = WHITESPACE_STR + '{"sub":"joe"}' + WHITESPACE_STR + assertTrue DefaultJwtParser.isLikelyJson(claimsJson.getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonWithLeadingTextBeforeJsonObject() { + // all whitespace chars as defined by Character.isWhitespace: + String claimsJson = ' x {"sub":"joe"}' + assertFalse DefaultJwtParser.isLikelyJson(claimsJson.getBytes(StandardCharsets.UTF_8)) + } + + @Test + void testIsLikelyJsonWithTrailingTextAfterJsonObject() { + // all whitespace chars as defined by Character.isWhitespace: + String claimsJson = '{"sub":"joe"} x' + assertFalse DefaultJwtParser.isLikelyJson(claimsJson.getBytes(StandardCharsets.UTF_8)) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy index fed984aa..28c4786d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -17,17 +17,55 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.Jwt import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Encoders import org.junit.Test +import java.nio.charset.StandardCharsets + import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotEquals class DefaultJwtTest { @Test void testToString() { - String compact = Jwts.builder().setHeaderParam('foo', 'bar').setAudience('jsmith').compact(); - Jwt jwt = Jwts.parserBuilder().build().parseClaimsJwt(compact); - assertEquals 'header={foo=bar, alg=none},body={aud=jsmith}', jwt.toString() + String compact = Jwts.builder().setHeaderParam('foo', 'bar').setAudience('jsmith').compact() + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseClaimsJwt(compact) + assertEquals 'header={foo=bar, alg=none},payload={aud=jsmith}', jwt.toString() } + @Test + void testByteArrayPayloadToString() { + byte[] bytes = 'hello JJWT'.getBytes(StandardCharsets.UTF_8) + String encoded = Encoders.BASE64URL.encode(bytes) + String compact = Jwts.builder().setHeaderParam('foo', 'bar').setContent(bytes).compact() + Jwt jwt = Jwts.parserBuilder().enableUnsecuredJws().build().parseContentJwt(compact) + assertEquals "header={foo=bar, alg=none},payload=$encoded" as String, jwt.toString() + } + + @Test + void testEqualsAndHashCode() { + String compact = Jwts.builder().claim('foo', 'bar').compact() + def parser = Jwts.parserBuilder().enableUnsecuredJws().build() + def jwt1 = parser.parseClaimsJwt(compact) + def jwt2 = parser.parseClaimsJwt(compact) + assertNotEquals jwt1, 'hello' as String + assertEquals jwt1, jwt1 + assertEquals jwt2, jwt2 + assertEquals jwt1, jwt2 + assertEquals jwt1.hashCode(), jwt2.hashCode() + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + @Test + void testBodyAndPayloadSame() { + String compact = Jwts.builder().claim('foo', 'bar').compact() + def parser = Jwts.parserBuilder().enableUnsecuredJws().build() + def jwt1 = parser.parseClaimsJwt(compact) + def jwt2 = parser.parseClaimsJwt(compact) + assertEquals jwt1.getBody(), jwt1.getPayload() + assertEquals jwt2.getBody(), jwt2.getPayload() + assertEquals jwt1.getBody(), jwt2.getBody() + assertEquals jwt1.getPayload(), jwt2.getPayload() + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultUnprotectedHeaderBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultUnprotectedHeaderBuilderTest.groovy new file mode 100644 index 00000000..b1b966fc --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultUnprotectedHeaderBuilderTest.groovy @@ -0,0 +1,85 @@ +/* + * 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 org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +class DefaultUnprotectedHeaderBuilderTest { + + static DefaultUnprotectedHeaderBuilder builder + + @Before + void setUp() { + builder = new DefaultUnprotectedHeaderBuilder() + } + + @Test + void testNewHeader() { + assertTrue builder.header instanceof DefaultUnprotectedHeader + } + + @Test + void testType() { + String type = 'foo' + assertEquals type, builder.setType(type).build().getType() + } + + @Test + void testContentType() { + String cty = 'text/plain' + assertEquals cty, builder.setContentType(cty).build().getContentType() + } + + @Test + void testAlgorithm() { + String alg = 'none' + assertEquals alg, builder.setAlgorithm(alg).build().getAlgorithm() + } + + @Test + void testCompressionAlgorithm() { + String zip = 'DEF' + assertEquals zip, builder.setCompressionAlgorithm(zip).build().getCompressionAlgorithm() + } + + @Test + void testPut() { + assertEquals 'bar', builder.put('foo', 'bar').build().get('foo') + } + + @Test + void testPutAll() { + def m = ['foo': 'bar', 'baz': 'bat'] + assertEquals m, builder.putAll(m).build() + } + + @Test + void testRemove() { + def header = builder.put('foo', 'bar').remove('foo').build() + assertTrue header.isEmpty() + } + + @Test + void testClear() { + def m = ['foo': 'bar', 'baz': 'bat'] + def header = builder.putAll(m).clear().build() + assertTrue header.isEmpty() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy new file mode 100644 index 00000000..7acd2f36 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/IdLocatorTest.groovy @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 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.UnsupportedJwtException +import io.jsonwebtoken.impl.lang.Field +import io.jsonwebtoken.impl.lang.Fields +import io.jsonwebtoken.impl.lang.Functions +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class IdLocatorTest { + + private static final Field TEST_FIELD = Fields.string('foo', 'Foo') + + @Test + void missingRequiredHeaderValueTest() { + def msg = 'foo is required' + def loc = new IdLocator(TEST_FIELD, 'foo is required', Functions.forNull()) + def header = new DefaultUnprotectedHeader() + try { + loc.apply(header) + fail() + } catch (MalformedJwtException expected) { + assertEquals msg, expected.getMessage() + } + } + + @Test + void unlocatableJwtHeaderInstanceTest() { + def loc = new IdLocator(TEST_FIELD, 'foo is required', Functions.forNull()) + def header = new DefaultUnprotectedHeader([foo: 'foo']) + try { + loc.apply(header) + } catch (UnsupportedJwtException expected) { + String msg = "Unrecognized JWT ${TEST_FIELD} header value: foo" + assertEquals msg, expected.getMessage() + } + } + + @Test + void unlocatableJwsHeaderInstanceTest() { + def loc = new IdLocator(TEST_FIELD, 'foo is required', Functions.forNull()) + def header = new DefaultJwsHeader([foo: 'foo']) + try { + loc.apply(header) + } catch (UnsupportedJwtException expected) { + String msg = "Unrecognized JWS ${TEST_FIELD} header value: foo" + assertEquals msg, expected.getMessage() + } + } + + @Test + void unlocatableJweHeaderInstanceTest() { + def loc = new IdLocator(TEST_FIELD, 'foo is required', Functions.forNull()) + def header = new DefaultJweHeader([foo: 'foo']) + try { + loc.apply(header) + } catch (UnsupportedJwtException expected) { + String msg = "Unrecognized JWE ${TEST_FIELD} header value: foo" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy index e95f3f3e..0ae6987e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/ImmutableJwtParserTest.groovy @@ -15,23 +15,16 @@ */ package io.jsonwebtoken.impl -import io.jsonwebtoken.Clock -import io.jsonwebtoken.CompressionCodecResolver -import io.jsonwebtoken.JwtHandler -import io.jsonwebtoken.JwtParser -import io.jsonwebtoken.SigningKeyResolver +import io.jsonwebtoken.* import io.jsonwebtoken.io.Decoder import io.jsonwebtoken.io.Deserializer import org.junit.Test import java.security.Key -import static org.easymock.EasyMock.expect -import static org.easymock.EasyMock.mock -import static org.easymock.EasyMock.replay -import static org.easymock.EasyMock.verify -import static org.hamcrest.MatcherAssert.assertThat +import static org.easymock.EasyMock.* import static org.hamcrest.CoreMatchers.is +import static org.hamcrest.MatcherAssert.assertThat /** * TODO: These mutable methods will be removed pre 1.0, and ImmutableJwtParser will be replaced with the default diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 3db94003..c8f9bc8e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -15,196 +15,91 @@ */ package io.jsonwebtoken.impl -import io.jsonwebtoken.lang.DateFormats +import io.jsonwebtoken.impl.lang.Field +import io.jsonwebtoken.impl.lang.Fields +import io.jsonwebtoken.impl.security.Randoms +import io.jsonwebtoken.lang.Collections +import org.junit.Before import org.junit.Test import static org.junit.Assert.* class JwtMapTest { - @Test - void testToDateFromNull() { - Date actual = JwtMap.toDate(null, 'foo') - assertNull actual - } + private static final Field DUMMY = Fields.string('' + Randoms.secureRandom().nextInt(), "RANDOM") + private static final Field SECRET = Fields.secretBigInt('foo', 'foo') + private static final Set> FIELDS = Collections.setOf(DUMMY) + JwtMap jwtMap - @Test - void testToDateFromDate() { - def d = new Date() - Date date = JwtMap.toDate(d, 'foo') - assertSame date, d - } - - @Test - void testToDateFromCalendar() { - def c = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def d = c.getTime() - Date date = JwtMap.toDate(c, 'foo') - assertEquals date, d - } - - @Test - void testToDateFromIso8601String() { - Date d = new Date(2015, 1, 1, 12, 0, 0) - String s = DateFormats.formatIso8601(d, false) - Date date = JwtMap.toDate(s, 'foo') - assertEquals date, d - } - - @Test - void testToDateFromInvalidIso8601String() { - Date d = new Date(2015, 1, 1, 12, 0, 0) - String s = d.toString() - try { - JwtMap.toDate(d.toString(), 'foo') - fail() - } catch (IllegalArgumentException iae) { - assertEquals "'foo' value does not appear to be ISO-8601-formatted: $s" as String, iae.getMessage() - } - } - - @Test - void testToDateFromIso8601MillisString() { - long millis = System.currentTimeMillis(); - Date d = new Date(millis) - String s = DateFormats.formatIso8601(d) - Date date = JwtMap.toDate(s, 'foo') - assertEquals date, d - } - - @Test - void testToSpecDateWithNull() { - assertNull JwtMap.toSpecDate(null, 'exp') - } - - @Test - void testToSpecDateWithLong() { - long millis = System.currentTimeMillis() - long seconds = (millis / 1000l) as long - Date d = new Date(seconds * 1000) - assertEquals d, JwtMap.toSpecDate(seconds, 'exp') - } - - @Test - void testToSpecDateWithString() { - Date d = new Date(2015, 1, 1, 12, 0, 0) - String s = (d.getTime() / 1000) + '' //JWT timestamps are in seconds - need to strip millis - Date date = JwtMap.toSpecDate(s, 'exp') - assertEquals date, d - } - - @Test - void testToSpecDateWithIso8601String() { - long millis = System.currentTimeMillis(); - Date d = new Date(millis) - String s = DateFormats.formatIso8601(d) - Date date = JwtMap.toSpecDate(s, 'exp') - assertEquals date, d - } - - @Test - void testToSpecDateWithDate() { - long millis = System.currentTimeMillis(); - Date d = new Date(millis) - Date date = JwtMap.toSpecDate(d, 'exp') - assertSame d, date - } - - @Deprecated //remove just before 1.0.0 - @Test - void testSetDate() { - def m = new JwtMap() - m.put('foo', 'bar') - m.setDate('foo', null) - assertNull m.get('foo') - long millis = System.currentTimeMillis() - long seconds = (millis / 1000l) as long - Date date = new Date(millis) - m.setDate('foo', date) - assertEquals seconds, m.get('foo') - } - - @Test - void testToDateFromNonDateObject() { - try { - JwtMap.toDate(new Object() { @Override public String toString() {return 'hi'} }, 'foo') - fail() - } catch (IllegalStateException iae) { - assertEquals iae.message, "Cannot create Date from 'foo' value 'hi'." - } + @Before + void setup() { + // dummy field to satisfy constructor: + jwtMap = new JwtMap(FIELDS) } @Test void testContainsKey() { - def m = new JwtMap() - m.put('foo', 'bar') - assertTrue m.containsKey('foo') + jwtMap.put('foo', 'bar') + assertTrue jwtMap.containsKey('foo') } @Test void testContainsValue() { - def m = new JwtMap() - m.put('foo', 'bar') - assertTrue m.containsValue('bar') + jwtMap.put('foo', 'bar') + assertTrue jwtMap.containsValue('bar') } @Test void testRemoveByPuttingNull() { - def m = new JwtMap() - m.put('foo', 'bar') - assertTrue m.containsKey('foo') - assertTrue m.containsValue('bar') - m.put('foo', null) - assertFalse m.containsKey('foo') - assertFalse m.containsValue('bar') + jwtMap.put('foo', 'bar') + assertTrue jwtMap.containsKey('foo') + assertTrue jwtMap.containsValue('bar') + jwtMap.put('foo', null) + assertFalse jwtMap.containsKey('foo') + assertFalse jwtMap.containsValue('bar') } @Test void testPutAll() { - def m = new JwtMap(); - m.putAll([a: 'b', c: 'd']) - assertEquals m.size(), 2 - assertEquals m.a, 'b' - assertEquals m.c, 'd' + jwtMap.putAll([a: 'b', c: 'd']) + assertEquals jwtMap.size(), 2 + assertEquals jwtMap.a, 'b' + assertEquals jwtMap.c, 'd' } @Test void testPutAllWithNullArgument() { - def m = new JwtMap(); - m.putAll((Map)null) - assertEquals m.size(), 0 + jwtMap.putAll((Map) null) + assertEquals jwtMap.size(), 0 } @Test void testClear() { - def m = new JwtMap(); - m.put('foo', 'bar') - assertEquals m.size(), 1 - m.clear() - assertEquals m.size(), 0 + jwtMap.put('foo', 'bar') + assertEquals jwtMap.size(), 1 + jwtMap.clear() + assertEquals jwtMap.size(), 0 } @Test void testKeySet() { - def m = new JwtMap() - m.putAll([a: 'b', c: 'd']) - assertEquals( m.keySet(), ['a', 'c'] as Set) + jwtMap.putAll([a: 'b', c: 'd']) + assertEquals(jwtMap.keySet(), ['a', 'c'] as Set) } @Test void testValues() { - def m = new JwtMap() - m.putAll([a: 'b', c: 'd']) + jwtMap.putAll([a: 'b', c: 'd']) def s = ['b', 'd'] - assertTrue m.values().containsAll(s) && s.containsAll(m.values()) + assertTrue jwtMap.values().containsAll(s) && s.containsAll(jwtMap.values()) } @Test void testEquals() throws Exception { - def m1 = new JwtMap(); + def m1 = new JwtMap(FIELDS); m1.put("a", "a"); - def m2 = new JwtMap(); + def m2 = new JwtMap(FIELDS); m2.put("a", "a"); assertEquals(m1, m2); @@ -212,14 +107,35 @@ class JwtMapTest { @Test void testHashcode() throws Exception { - def m = new JwtMap(); - def hashCodeEmpty = m.hashCode(); + def hashCodeEmpty = jwtMap.hashCode(); - m.put("a", "b"); - def hashCodeNonEmpty = m.hashCode(); + jwtMap.put("a", "b"); + def hashCodeNonEmpty = jwtMap.hashCode(); assertTrue(hashCodeEmpty != hashCodeNonEmpty); - def identityHash = System.identityHashCode(m); + def identityHash = System.identityHashCode(jwtMap); assertTrue(hashCodeNonEmpty != identityHash); } + + @Test + void testGetName() { + def map = new JwtMap(FIELDS) + assertEquals 'Map', map.getName() + } + + @Test + void testSetSecretFieldWithInvalidTypeValue() { + def map = new JwtMap(Collections.setOf(SECRET)) + def invalidValue = URI.create('https://whatever.com') + try { + map.put('foo', invalidValue) + fail() + } catch (IllegalArgumentException expected) { + //Ensure message so we don't show any secret value: + String msg = 'Invalid Map \'foo\' (foo) value: . Values must be ' + + 'either String or java.math.BigInteger instances. Value type found: ' + + 'java.net.URI.' + assertEquals msg, expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy new file mode 100644 index 00000000..743071ef --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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 org.junit.Test + +import static org.junit.Assert.* + +class JwtTokenizerTest { + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlHeader() { + def input = 'header .body.signature' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlBody() { + def input = 'header. body.signature' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlSignature() { + def input = 'header.body. signature' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlJweBody() { + def input = 'header.encryptedKey.initializationVector. body.authenticationTag' + new JwtTokenizer().tokenize(input) + } + + @Test(expected= MalformedJwtException) + void testParseWithWhitespaceInBase64UrlJweTag() { + def input = 'header.encryptedKey.initializationVector.body. authenticationTag' + new JwtTokenizer().tokenize(input) + } + + @Test + void testJwe() { + + def input = 'header.encryptedKey.initializationVector.body.authenticationTag' + + def t = new JwtTokenizer().tokenize(input) + + assertNotNull t + assertTrue t instanceof TokenizedJwe + TokenizedJwe tjwe = (TokenizedJwe)t + assertEquals 'header', tjwe.getProtected() + assertEquals 'encryptedKey', tjwe.getEncryptedKey() + assertEquals 'initializationVector', tjwe.getIv() + assertEquals 'body', tjwe.getBody() + assertEquals 'authenticationTag', tjwe.getDigest() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy new file mode 100644 index 00000000..87f3fbfe --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 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.impl.security.Randoms +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.jackson.io.JacksonDeserializer + +import java.nio.charset.StandardCharsets + +class RfcTests { + + static final Deserializer> JSON_DESERIALIZER = new JacksonDeserializer<>() + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + static final String stripws(String s) { + return s.replaceAll('[\\s]', '') + } + + static final Map jsonToMap(String json) { + byte[] bytes = json.getBytes(StandardCharsets.UTF_8) + return JSON_DESERIALIZER.deserialize(bytes) + } + + /** + * Returns a random string useful as a test value NOT to be used as a cryptographic key. + * @return a random string useful as a test value NOT to be used as a cryptographic key. + */ + static String srandom() { + byte[] random = new byte[16] + Randoms.secureRandom().nextBytes(random) + return Encoders.BASE64URL.encode(random) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy index a1e17436..1fff6dd4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy @@ -19,37 +19,48 @@ import io.jsonwebtoken.CompressionCodec import io.jsonwebtoken.CompressionException import org.junit.Test +import static org.junit.Assert.assertEquals + /** * @since 0.6.0 */ class AbstractCompressionCodecTest { static class ExceptionThrowingCodec extends AbstractCompressionCodec { + ExceptionThrowingCodec() { + super("Test") + } + @Override protected byte[] doCompress(byte[] payload) throws IOException { throw new IOException("Test Exception") } - @Override - String getAlgorithmName() { - return "Test" - } - @Override protected byte[] doDecompress(byte[] payload) throws IOException { - throw new IOException("Test Decompress Exception"); + throw new IOException("Test Decompress Exception") } } @Test(expected = CompressionException.class) void testCompressWithException() { - CompressionCodec codecUT = new ExceptionThrowingCodec(); - codecUT.compress(new byte[0]); + CompressionCodec codecUT = new ExceptionThrowingCodec() + codecUT.compress(new byte[0]) } @Test(expected = CompressionException.class) void testDecompressWithException() { - CompressionCodec codecUT = new ExceptionThrowingCodec(); - codecUT.decompress(new byte[0]); + CompressionCodec codecUT = new ExceptionThrowingCodec() + codecUT.decompress(new byte[0]) + } + + @Test + void testGetId() { + assertEquals "Test", new ExceptionThrowingCodec().getId() + } + + @Test + void testAlgorithmName() { + assertEquals "Test", new ExceptionThrowingCodec().getAlgorithmName() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy index 6115e234..e3ae2d21 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolverTest.groovy @@ -16,19 +16,16 @@ package io.jsonwebtoken.impl.compression import io.jsonwebtoken.CompressionCodec +import io.jsonwebtoken.CompressionCodecs import io.jsonwebtoken.CompressionException -import io.jsonwebtoken.impl.DefaultHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.DefaultUnprotectedHeader import io.jsonwebtoken.impl.io.FakeServiceDescriptorClassLoader import io.jsonwebtoken.impl.lang.Services import org.junit.Assert import org.junit.Test -import io.jsonwebtoken.CompressionCodecs - -import static org.hamcrest.CoreMatchers.hasItem -import static org.hamcrest.CoreMatchers.instanceOf -import static org.hamcrest.CoreMatchers.is -import static org.hamcrest.CoreMatchers.nullValue +import static org.hamcrest.CoreMatchers.* import static org.hamcrest.MatcherAssert.assertThat class DefaultCompressionCodecResolverTest { @@ -36,18 +33,18 @@ class DefaultCompressionCodecResolverTest { @Test void resolveHeaderTest() { assertThat new DefaultCompressionCodecResolver().resolveCompressionCodec( - new DefaultHeader()), nullValue() + new DefaultUnprotectedHeader()), nullValue() assertThat new DefaultCompressionCodecResolver().resolveCompressionCodec( - new DefaultHeader().setCompressionAlgorithm("def")), is(CompressionCodecs.DEFLATE) + new DefaultUnprotectedHeader().setCompressionAlgorithm("def")), is(CompressionCodecs.DEFLATE) assertThat new DefaultCompressionCodecResolver().resolveCompressionCodec( - new DefaultHeader().setCompressionAlgorithm("gzip")), is(CompressionCodecs.GZIP) + new DefaultUnprotectedHeader().setCompressionAlgorithm("gzip")), is(CompressionCodecs.GZIP) } @Test void invalidCompressionNameTest() { try { new DefaultCompressionCodecResolver().resolveCompressionCodec( - new DefaultHeader().setCompressionAlgorithm("expected-missing")) + new DefaultUnprotectedHeader().setCompressionAlgorithm("expected-missing")) Assert.fail("Expected CompressionException to be thrown") } catch (CompressionException e) { assertThat e.message, is(String.format(DefaultCompressionCodecResolver.MISSING_COMPRESSION_MESSAGE, "expected-missing")) @@ -55,24 +52,15 @@ class DefaultCompressionCodecResolverTest { } @Test - void overrideDefaultCompressionImplTest() { + void testCustomCompressionCodecServiceDoesNotOverrideStandardCodecs() { FakeServiceDescriptorClassLoader.runWithFake "io.jsonwebtoken.io.compression.CompressionCodec.test.override", { // first make sure the service loader actually resolves the test class assertThat Services.loadAll(CompressionCodec), hasItem(instanceOf(YagCompressionCodec)) + def header = new DefaultJwsHeader(['zip': 'gzip']) // now we know the class is loadable, make sure we ALWAYS return the GZIP impl - assertThat new DefaultCompressionCodecResolver().byName("gzip"), instanceOf(GzipCompressionCodec) - } - } - - @Test - void emptyCompressionAlgInHeaderTest() { - try { - new DefaultCompressionCodecResolver().byName("") - Assert.fail("Expected IllegalArgumentException to be thrown") - } catch (IllegalArgumentException e) { - // expected + assertThat new DefaultCompressionCodecResolver().locate(header), instanceOf(GzipCompressionCodec) } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy index 52f2b4e1..9c05ee75 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2019 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.compression import io.jsonwebtoken.CompressionException @@ -18,7 +33,7 @@ class DeflateCompressionCodecTest { @Test void testBackwardsCompatibility_0_10_6() { final String jwtFrom0106 = 'eyJhbGciOiJub25lIiwiemlwIjoiREVGIn0.eNqqVsosLlayUspNVdJRKi5NAjJLi1OLgJzMxBIlK0sTMzMLEwsDAx2l1IoCJSsTQwMjExOQQC0AAAD__w.' - Jwts.parserBuilder().build().parseClaimsJwt(jwtFrom0106) // no exception should be thrown + Jwts.parserBuilder().enableUnsecuredJws().enableUnsecuredDecompression().build().parseClaimsJwt(jwtFrom0106) // no exception should be thrown } /** diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy index 52ffe875..601226bf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/YagCompressionCodec.groovy @@ -24,12 +24,17 @@ import io.jsonwebtoken.CompressionException class YagCompressionCodec implements CompressionCodec { @Override - String getAlgorithmName() { - return new GzipCompressionCodec().getAlgorithmName(); + String getId() { + return GzipCompressionCodec.GZIP } @Override - byte[] compress(byte[] payload) throws CompressionException { + String getAlgorithmName() { + return GzipCompressionCodec.GZIP + } + + @Override + byte[] compress(byte[] content) throws CompressionException { return new byte[0] } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy deleted file mode 100644 index 5362d40c..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertSame - -class DefaultJwtSignatureValidatorTest { - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - void testDeprecatedTwoArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def validator = new DefaultJwtSignatureValidator(alg, key) - - assertNotNull validator.signatureValidator - assertSame Decoders.BASE64URL, validator.base64UrlDecoder - } - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - void testDeprecatedThreeArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def validator = new DefaultJwtSignatureValidator(DefaultSignatureValidatorFactory.INSTANCE, alg, key) - - assertNotNull validator.signatureValidator - assertSame Decoders.BASE64URL, validator.base64UrlDecoder - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy deleted file mode 100644 index e0f1b7ea..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Encoders -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import static org.junit.Assert.assertNotNull -import static org.junit.Assert.assertSame - -class DefaultJwtSignerTest { - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - //remove just before 1.0.0 release - void testDeprecatedTwoArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def signer = new DefaultJwtSigner(alg, key) - - assertNotNull signer.signer - assertSame Encoders.BASE64URL, signer.base64UrlEncoder - } - - @Test - //TODO: remove this before 1.0 since it tests a deprecated method - @Deprecated - //remove just before 1.0.0 release - void testDeprecatedThreeArgCtor() { - - def alg = SignatureAlgorithm.HS256 - def key = Keys.secretKeyFor(alg) - def signer = new DefaultJwtSigner(DefaultSignerFactory.INSTANCE, alg, key) - - assertNotNull signer.signer - assertSame Encoders.BASE64URL, signer.base64UrlEncoder - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy deleted file mode 100644 index ccebf30b..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.InvalidKeyException -import io.jsonwebtoken.security.Keys -import org.junit.Test - -import java.security.KeyPair -import java.security.NoSuchProviderException -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.ECPublicKey - -import static org.junit.Assert.* - -class EllipticCurveProviderTest { - - @Test - void testGenerateKeyPair() { - KeyPair pair = EllipticCurveProvider.generateKeyPair() - assertNotNull pair - assertTrue pair.public instanceof ECPublicKey - assertTrue pair.private instanceof ECPrivateKey - } - - @Test - void testGenerateKeyPairWithInvalidProviderName() { - try { - EllipticCurveProvider.generateKeyPair("EC", "Foo", SignatureAlgorithm.ES256, null) - fail() - } catch (IllegalStateException ise) { - assertEquals ise.message, "Unable to generate Elliptic Curve KeyPair: no such provider: Foo" - assertTrue ise.cause instanceof NoSuchProviderException - } - } - - @Test - void testGenerateKeyPairWithNullAlgorithm() { - try { - EllipticCurveProvider.generateKeyPair("EC", "Foo", null, null) - fail() - } catch (IllegalArgumentException ise) { - assertEquals ise.message, "SignatureAlgorithm argument cannot be null." - } - } - - @Test - void testGenerateKeyPairWithNonEllipticCurveAlgorithm() { - try { - EllipticCurveProvider.generateKeyPair("EC", "Foo", SignatureAlgorithm.HS256, null) - fail() - } catch (IllegalArgumentException ise) { - assertEquals ise.message, "SignatureAlgorithm argument must represent an Elliptic Curve algorithm." - } - } - - @Test - void testConstructorWithNonEcKey() { - def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) - try { - new EllipticCurveProvider(SignatureAlgorithm.ES256, key) {} - } catch (InvalidKeyException expected) { - String msg = 'Elliptic Curve signatures require an ECKey. The provided key of type ' + - 'javax.crypto.spec.SecretKeySpec is not a java.security.interfaces.ECKey instance.' - assertEquals msg, expected.getMessage() - } - } - - @Test - void testConstructorWithInvalidKeyFieldLength() { - def keypair = Keys.keyPairFor(SignatureAlgorithm.ES256) - try { - new EllipticCurveProvider(SignatureAlgorithm.ES384, keypair.public){} - } catch (InvalidKeyException expected) { - String msg = "EllipticCurve key has a field size of 32 bytes (256 bits), but ES384 requires a " + - "field size of 48 bytes (384 bits) per [RFC 7518, Section 3.4 (validation)]" + - "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." - assertEquals msg, expected.getMessage() - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy deleted file mode 100644 index 51aba2e9..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.JwtException -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Decoders -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.InvalidKeyException -import java.security.KeyFactory -import java.security.PublicKey -import java.security.Signature -import java.security.spec.X509EncodedKeySpec - -import static org.junit.Assert.* - -class EllipticCurveSignatureValidatorTest { - - @Test - void testDoVerifyWithInvalidKeyException() { - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - def alg = SignatureAlgorithm.ES512 - def keypair = EllipticCurveProvider.generateKeyPair(alg) - - def v = new EllipticCurveSignatureValidator(alg, EllipticCurveProvider.generateKeyPair(alg).public) { - @Override - protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException { - throw ex; - } - } - - byte[] data = new byte[32] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(data) - - byte[] signature = new EllipticCurveSigner(alg, keypair.getPrivate()).sign(data) - - try { - v.isValid(data, signature) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to verify Elliptic Curve signature using configured ECPublicKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void ecdsaSignatureComplianceTest() { - def fact = KeyFactory.getInstance("EC"); - def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" - def pub = fact.generatePublic(new X509EncodedKeySpec(Decoders.BASE64.decode(publicKey))) - def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub) - def verifier = { token -> - def signatureStart = token.lastIndexOf('.') - def withoutSignature = token.substring(0, signatureStart) - def signature = token.substring(signatureStart + 1) - assert v.isValid(withoutSignature.getBytes("US-ASCII"), Decoders.BASE64URL.decode(signature)), "Signature do not match that of other implementations" - } - //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 - verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") - //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 - verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") - } - - @Test // asserts guard for JVM security bug CVE-2022-21449: - void legacySignatureCompatDefaultTest() { - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def alg = SignatureAlgorithm.ES512 - def signature = Signature.getInstance(alg.jcaName) - def data = withoutSignature.getBytes("US-ASCII") - signature.initSign(keypair.private) - signature.update(data) - def signed = signature.sign() - def validator = new EllipticCurveSignatureValidator(alg, keypair.public) - try { - validator.isValid(data, signed) - fail() - } catch (SignatureException expected) { - String signedBytesString = EllipticCurveProvider.byteSizeString(signed.length) - String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. Provided " + - "signature is $signedBytesString but ES512 signatures must be exactly 132 bytes (1056 bits) " + - "per [RFC 7518, Section 3.4 (validation)]" + - "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String - assertEquals msg, expected.getMessage() - } - } - - @Test - void legacySignatureCompatWhenEnabledTest() { - try { - System.setProperty(EllipticCurveSignatureValidator.DER_ENCODING_SYS_PROPERTY_NAME, 'true') - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def signature = Signature.getInstance(SignatureAlgorithm.ES512.jcaName) - def data = withoutSignature.getBytes("US-ASCII") - signature.initSign(keypair.private) - signature.update(data) - def signed = signature.sign() - assertTrue new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, keypair.public).isValid(data, signed) - } finally { - System.clearProperty(EllipticCurveSignatureValidator.DER_ENCODING_SYS_PROPERTY_NAME) - } - } - - @Test // asserts guard for JVM security bug CVE-2022-21449: - void testSignatureAllZeros() { - byte[] forgedSig = new byte[64] - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def alg = SignatureAlgorithm.ES256 - def keypair = EllipticCurveProvider.generateKeyPair(alg) - def data = withoutSignature.getBytes("US-ASCII") - def validator = new EllipticCurveSignatureValidator(alg, keypair.public) - assertFalse validator.isValid(data, forgedSig) - } - - @Test // asserts guard for JVM security bug CVE-2022-21449: - void testSignatureRZero() { - byte[] r = new byte[32] - byte[] s = new byte[32]; Arrays.fill(s, Byte.MAX_VALUE) - byte[] sig = new byte[r.length + s.length] - System.arraycopy(r, 0, sig, 0, r.length) - System.arraycopy(s, 0, sig, r.length, s.length) - - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) - def data = withoutSignature.getBytes("US-ASCII") - def validator = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public) - assertFalse validator.isValid(data, sig) - } - - @Test // asserts guard for JVM security bug CVE-2022-21449: - void testSignatureSZero() { - byte[] r = new byte[32]; Arrays.fill(r, Byte.MAX_VALUE); - byte[] s = new byte[32] - byte[] sig = new byte[r.length + s.length] - System.arraycopy(r, 0, sig, 0, r.length) - System.arraycopy(s, 0, sig, r.length, s.length) - - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) - def data = withoutSignature.getBytes("US-ASCII") - def validator = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public) - assertFalse validator.isValid(data, sig) - } - - @Test // asserts guard for JVM security bug CVE-2022-21449: - void ecdsaInvalidSignatureValuesTest() { - def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" - def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) - def data = withoutSignature.getBytes("US-ASCII") - def invalidSignature = Decoders.BASE64URL.decode(invalidEncodedSignature) - def validator = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public) - assertFalse("Forged signature must not be considered valid.", validator.isValid(data, invalidSignature)) - } - - @Test - void invalidAlgorithmTest() { - def invalidAlgorithm = SignatureAlgorithm.HS256 - try { - EllipticCurveProvider.getSignatureByteArrayLength(invalidAlgorithm) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Unsupported Algorithm: ' + invalidAlgorithm.name() - } - } - - @Test - void invalidECDSASignatureFormatTest() { - try { - def signature = new byte[257] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - EllipticCurveProvider.transcodeConcatToDER(signature) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format.' - } - } - - @Test - void invalidDERSignatureToJoseFormatTest() { - def verify = { signature -> - try { - EllipticCurveProvider.transcodeDERToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - def signature = new byte[257] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - //invalid type - signature[0] = 34 - verify(signature) - def shortSignature = new byte[7] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(shortSignature) - verify(shortSignature) - signature[0] = 48 -// signature[1] = 0x81 - signature[1] = -10 - verify(signature) - } - - @Test - void testPaddedSignatureToDER() { - def signature = new byte[32] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - signature[0] = 0 as byte - EllipticCurveProvider.transcodeConcatToDER(signature) //no exception - } - - @Test - void edgeCaseSignatureToConcatLengthTest() { - try { - def signature = Decoders.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") - EllipticCurveProvider.transcodeDERToConcat(signature, 132) - fail() - } catch (JwtException e) { - - } - } - - @Test - void edgeCaseSignatureToConcatInvalidSignatureTest() { - try { - def signature = Decoders.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeDERToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { - try { - def signature = Decoders.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeDERToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { - try { - def signature = Decoders.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeDERToConcat(signature, 132) - fail() - } catch (JwtException e) { - assertEquals e.message, 'Invalid ECDSA signature format' - } - } - - @Test - void verifySwarmTest() { - for (SignatureAlgorithm algorithm : [SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512]) { - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair(algorithm) - def data = withoutSignature.getBytes("US-ASCII") - def signature = new EllipticCurveSigner(algorithm, keypair.private).sign(data) - assert new EllipticCurveSignatureValidator(algorithm, keypair.public).isValid(data, signature) - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy deleted file mode 100644 index d8a27c06..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.JwtException -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.InvalidKeyException -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey - -import static org.junit.Assert.* - -class EllipticCurveSignerTest { - - @Test - void testConstructorWithoutECAlg() { - SignatureAlgorithm alg = SignatureAlgorithm.HS256 - try { - new EllipticCurveSigner(alg, Keys.secretKeyFor(alg)) - fail('EllipticCurveSigner should reject non ECPrivateKeys') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, 'SignatureAlgorithm must be an Elliptic Curve algorithm.' - } - } - - @Test - void testConstructorWithoutECPrivateKey() { - def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) - try { - new EllipticCurveSigner(SignatureAlgorithm.ES256, pair.public) - fail('EllipticCurveSigner should reject public ECKey instances.') - } catch (io.jsonwebtoken.security.InvalidKeyException expected) { - String msg = 'Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of ' + - 'type sun.security.ec.ECPublicKeyImpl is not an EC PrivateKey.' - assertEquals(msg, expected.getMessage()) - } - } - - @Test - void testDoSignWithInvalidKeyException() { - - SignatureAlgorithm alg = SignatureAlgorithm.ES256 - - KeyPair kp = Keys.keyPairFor(alg) - PrivateKey privateKey = kp.getPrivate() - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - def signer = new EllipticCurveSigner(alg, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Invalid Elliptic Curve PrivateKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testDoSignWithJoseSignatureFormatException() { - - SignatureAlgorithm alg = SignatureAlgorithm.ES256 - KeyPair kp = EllipticCurveProvider.generateKeyPair(alg) - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final JwtException ex = new JwtException(msg) - - def signer = new EllipticCurveSigner(alg, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { - throw ex - } - } - - byte[] bytes = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to convert signature to JOSE format. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testDoSignWithJdkSignatureException() { - - SignatureAlgorithm alg = SignatureAlgorithm.ES256 - KeyPair kp = EllipticCurveProvider.generateKeyPair(alg) - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final java.security.SignatureException ex = new java.security.SignatureException(msg) - - def signer = new EllipticCurveSigner(alg, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to calculate signature using Elliptic Curve PrivateKey. ' + msg - assertSame se.cause, ex - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy deleted file mode 100644 index 086028cd..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import org.junit.Test - -import javax.crypto.SecretKey - -import static org.junit.Assert.assertEquals - -class MacProviderTest { - - private void testHmac(SignatureAlgorithm alg) { - testHmac(alg, MacProvider.generateKey(alg)) - } - - private void testHmac(SignatureAlgorithm alg, SecretKey key) { - assertEquals alg.jcaName, key.algorithm - assertEquals alg.digestLength / 8 as int, key.encoded.length - } - - @Test - void testDefault() { - testHmac(SignatureAlgorithm.HS512, MacProvider.generateKey()) - } - - @Test - void testHS256() { - testHmac(SignatureAlgorithm.HS256) - } - - @Test - void testHS384() { - testHmac(SignatureAlgorithm.HS384) - } - - @Test - void testHS512() { - testHmac(SignatureAlgorithm.HS512) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy deleted file mode 100644 index ab6ebca7..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import javax.crypto.Mac -import java.security.InvalidKeyException -import java.security.Key -import java.security.NoSuchAlgorithmException - -import static org.junit.Assert.* - -class MacSignerTest { - - private static final Random rng = new Random(); //doesn't need to be secure - we're just testing - - @Test - void testCtorArgNotASecretKey() { - - def key = new Key() { - @Override - String getAlgorithm() { - return null - } - - @Override - String getFormat() { - return null - } - - @Override - byte[] getEncoded() { - return new byte[0] - } - } - - try { - new MacSigner(SignatureAlgorithm.HS256, key) - fail() - } catch (IllegalArgumentException expected) { - } - } - - @Test - void testNoSuchAlgorithmException() { - byte[] key = new byte[32]; - byte[] data = new byte[32]; - rng.nextBytes(key); - rng.nextBytes(data); - - def s = new MacSigner(SignatureAlgorithm.HS256, key) { - @Override - protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { - throw new NoSuchAlgorithmException("foo"); - } - } - try { - s.sign(data); - fail(); - } catch (SignatureException e) { - assertTrue e.cause instanceof NoSuchAlgorithmException - assertEquals e.cause.message, 'foo' - } - } - - @Test - void testInvalidKeyException() { - byte[] key = new byte[32]; - byte[] data = new byte[32]; - rng.nextBytes(key); - rng.nextBytes(data); - - def s = new MacSigner(SignatureAlgorithm.HS256, key) { - @Override - protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { - throw new InvalidKeyException("foo"); - } - } - try { - s.sign(data); - fail(); - } catch (SignatureException e) { - assertTrue e.cause instanceof InvalidKeyException - assertEquals e.cause.message, 'foo' - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy deleted file mode 100644 index 4058d422..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2014 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner - -import javax.crypto.KeyGenerator -import java.security.NoSuchAlgorithmException - -import static org.easymock.EasyMock.eq -import static org.easymock.EasyMock.expect -import static org.junit.Assert.* -import static org.powermock.api.easymock.PowerMock.* - -/** - * This needs to be a separate class beyond MacProviderTest because it mocks the KeyGenerator class which messes up - * the other implementation tests in MacProviderTest. - */ -@RunWith(PowerMockRunner.class) -@PrepareForTest([KeyGenerator]) -class PowermockMacProviderTest { - - @Test - void testNoSuchAlgorithm() { - - mockStatic(KeyGenerator) - - def alg = SignatureAlgorithm.HS256 - def ex = new NoSuchAlgorithmException('foo') - - expect(KeyGenerator.getInstance(eq(alg.jcaName))).andThrow(ex) - - replay KeyGenerator - - try { - MacProvider.generateKey(alg) - fail() - } catch (IllegalStateException e) { - assertEquals 'The HmacSHA256 algorithm is not available. This should never happen on JDK 7 or later - ' + - 'please report this to the JJWT developers.', e.message - assertSame ex, e.getCause() - } - - verify KeyGenerator - - reset KeyGenerator - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy deleted file mode 100644 index 5d27a295..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.InvalidAlgorithmParameterException -import java.security.KeyPair -import java.security.Signature -import java.security.interfaces.RSAPrivateKey -import java.security.interfaces.RSAPublicKey -import java.security.spec.PSSParameterSpec - -import static org.junit.Assert.* - -class RsaProviderTest { - - @Test - void testGenerateKeyPair() { - KeyPair pair = RsaProvider.generateKeyPair() - assertNotNull pair - assertTrue pair.public instanceof RSAPublicKey - assertTrue pair.private instanceof RSAPrivateKey - } - - @Test - void testGenerateKeyPairWithInvalidProviderName() { - try { - RsaProvider.generateKeyPair('foo', 1024, SignatureProvider.DEFAULT_SECURE_RANDOM) - fail() - } catch (IllegalStateException ise) { - assertTrue ise.message.startsWith("Unable to obtain an RSA KeyPairGenerator: ") - } - } - - @Test - void testCreateSignatureInstanceWithInvalidPSSParameterSpecAlgorithm() { - - def p = new RsaProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair(512).public) { - @Override - protected void doSetParameter(Signature sig, PSSParameterSpec spec) throws InvalidAlgorithmParameterException { - throw new InvalidAlgorithmParameterException('foo') - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertTrue se.message.startsWith('Unsupported RSASSA-PSS parameter') - assertEquals se.cause.message, 'foo' - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy deleted file mode 100644 index 99bf2ea4..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.* - -import static org.junit.Assert.* - -class RsaSignatureValidatorTest { - - private static final Random rng = new Random(); //doesn't need to be secure - we're just testing - - @Test - void testConstructorWithNonRsaKey() { - try { - new RsaSignatureValidator(SignatureAlgorithm.RS256, Keys.secretKeyFor(SignatureAlgorithm.HS256)); - fail() - } catch (IllegalArgumentException iae) { - assertEquals "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance.", iae.message - } - } - - @Test - void testDoVerifyWithInvalidKeyException() { - - SignatureAlgorithm alg = SignatureAlgorithm.RS256 - - KeyPair kp = Keys.keyPairFor(alg) - PublicKey publicKey = kp.getPublic() - PrivateKey privateKey = kp.getPrivate() - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - RsaSignatureValidator v = new RsaSignatureValidator(alg, publicKey) { - @Override - protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException { - throw ex; - } - } - - byte[] bytes = new byte[16] - byte[] signature = new byte[16] - rng.nextBytes(bytes) - rng.nextBytes(signature) - - try { - v.isValid(bytes, signature) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to verify RSA signature using configured PublicKey. ' + msg - assertSame se.cause, ex - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy deleted file mode 100644 index d0470636..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import javax.crypto.spec.SecretKeySpec -import java.security.* - -import static org.junit.Assert.* - -class RsaSignerTest { - - private static final Random rng = new Random(); //doesn't need to be secure - we're just testing - - @Test - void testConstructorWithoutRsaAlg() { - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - SecretKeySpec key = new SecretKeySpec(bytes, 'HmacSHA256') - - try { - new RsaSigner(SignatureAlgorithm.HS256, key); - fail('RsaSigner should reject non RSA algorithms.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, 'SignatureAlgorithm must be an RSASSA or RSASSA-PSS algorithm.'; - } - } - - @Test - void testConstructorWithoutPrivateKey() { - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - SecretKeySpec key = new SecretKeySpec(bytes, 'HmacSHA256') - - try { - //noinspection GroovyResultOfObjectAllocationIgnored - new RsaSigner(SignatureAlgorithm.RS256, key); - fail('RsaSigner should reject non RSAPrivateKey instances.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSA PrivateKey."; - } - } - - @Test - void testConstructorWithoutRSAKey() { - - //private key, but not an RSAKey instance: - PrivateKey key = new PrivateKey() { - @Override - String getAlgorithm() { - return null - } - - @Override - String getFormat() { - return null - } - - @Override - byte[] getEncoded() { - return new byte[0] - } - } - - try { - //noinspection GroovyResultOfObjectAllocationIgnored - new RsaSigner(SignatureAlgorithm.RS256, key); - fail('RsaSigner should reject non RSAPrivateKey instances.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSA PrivateKey."; - } - } - - @Test - void testDoSignWithInvalidKeyException() { - - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - - KeyPair kp = keyGenerator.genKeyPair(); - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final InvalidKeyException ex = new InvalidKeyException(msg) - - RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Invalid RSA PrivateKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testDoSignWithJdkSignatureException() { - - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - - KeyPair kp = keyGenerator.genKeyPair(); - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); - - String msg = 'foo' - final java.security.SignatureException ex = new java.security.SignatureException(msg) - - RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey) { - @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { - throw ex - } - } - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - - try { - signer.sign(bytes) - fail(); - } catch (SignatureException se) { - assertEquals se.message, 'Unable to calculate signature using RSA PrivateKey. ' + msg - assertSame se.cause, ex - } - } - - @Test - void testSignSuccessful() { - - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); - - KeyPair kp = keyGenerator.genKeyPair(); - PrivateKey privateKey = kp.getPrivate(); - - byte[] bytes = new byte[16] - rng.nextBytes(bytes) - - RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey); - byte[] out1 = signer.sign(bytes) - - byte[] out2 = signer.sign(bytes) - - assertTrue(MessageDigest.isEqual(out1, out2)) - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy deleted file mode 100644 index 55fa2e9e..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2015 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.crypto - -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.SignatureException -import org.junit.Test - -import java.security.NoSuchAlgorithmException -import java.security.Signature - -import static org.junit.Assert.* - -class SignatureProviderTest { - - @Test - void testCreateSignatureInstanceNoSuchAlgorithm() { - - def p = new SignatureProvider(SignatureAlgorithm.HS256, MacProvider.generateKey()) { - @Override - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('foo') - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertEquals se.cause.message, 'foo' - } - } - - @Test - void testCreateSignatureInstanceNoSuchAlgorithmNonStandardAlgorithm() { - - def p = new SignatureProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair().getPrivate()) { - @Override - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('foo') - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertEquals se.cause.message, 'foo' - } - } - - @Test - void testCreateSignatureInstanceNoSuchAlgorithmNonStandardAlgorithmWithoutBouncyCastle() { - - def p = new SignatureProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair().getPrivate()) { - @Override - protected Signature getSignatureInstance() throws NoSuchAlgorithmException { - throw new NoSuchAlgorithmException('foo') - } - - @Override - protected boolean isBouncyCastleAvailable() { - return false - } - } - - try { - p.createSignatureInstance() - fail() - } catch (SignatureException se) { - assertTrue se.message.contains('This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath.') - } - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy new file mode 100644 index 00000000..681400bc --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/io/CodecTest.groovy @@ -0,0 +1,38 @@ +/* + * 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.io.DecodingException +import org.junit.Test + +import static org.junit.Assert.* + +class CodecTest { + + @Test + void testDecodingExceptionThrowsIAE() { + String s = 't#t' + try { + Codec.BASE64URL.applyFrom(s) + fail() + } catch (IllegalArgumentException expected) { + def cause = expected.getCause() + assertTrue cause instanceof DecodingException + String msg = "Cannot decode input String. Cause: ${cause.getMessage()}" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy new file mode 100644 index 00000000..0e1c5652 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BigIntegerUBytesConverterTest.groovy @@ -0,0 +1,75 @@ +/* + * 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.lang + +import io.jsonwebtoken.io.Decoders +import org.junit.Test + +import static org.junit.Assert.* + +class BigIntegerUBytesConverterTest { + + private BigIntegerUBytesConverter CONVERTER = Converters.BIGINT_UBYTES as BigIntegerUBytesConverter + + @Test + void testNegative() { + try { + CONVERTER.applyTo(BigInteger.valueOf(-1)) + fail() + } catch (IllegalArgumentException expected) { + assertEquals BigIntegerUBytesConverter.NEGATIVE_MSG, expected.getMessage() + } + } + + @Test + void testZero() { + byte[] result = CONVERTER.applyTo(BigInteger.ZERO) + assertEquals 1, result.length + assertTrue result[0] == 0x00 as byte + } + + @Test + void testStripSignByte() { + BigInteger val = BigInteger.valueOf(128) + byte[] bytes = val.toByteArray() + byte[] result = CONVERTER.applyTo(val) + assertEquals bytes.length - 1, result.length + } + + /** + * Asserts security considerations in https://www.rfc-editor.org/rfc/rfc7638#section-7, 4th paragraph. + */ + @Test + void testStripLeadingZeroBytes() { + + byte[] bytes1 = Decoders.BASE64URL.decode("AAEAAQ") + byte[] bytes2 = Decoders.BASE64URL.decode("AQAB") + + BigInteger bi1 = CONVERTER.applyFrom(bytes1) + BigInteger bi2 = CONVERTER.applyFrom(bytes2) + + assertEquals bi1, bi2 + } + + /** + * Asserts https://www.rfc-editor.org/rfc/rfc7518.html#section-2, 'Base64urlUInt' definition, last sentence: + *
    Zero is represented as BASE64URL(single zero-valued octet), which is "AA".
    + */ + @Test + void testZeroProducesAABase64Url() { + assertEquals 'AA', Converters.BIGINT.applyTo(BigInteger.ZERO) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy new file mode 100644 index 00000000..5f1a6913 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy @@ -0,0 +1,244 @@ +/* + * 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.lang + +import io.jsonwebtoken.impl.security.Randoms +import org.junit.Test + +import java.security.MessageDigest + +import static org.junit.Assert.* + +class BytesTest { + + static final Random RANDOM = Randoms.secureRandom() + + static final byte[] A = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05] as byte[] + static final byte[] B = [0x02, 0x03] as byte[] + static final byte[] C = [0x05, 0x06] as byte[] + static final byte[] D = [0x06, 0x07] as byte[] + + @Test + void testPrivateCtor() { // for code coverage only + new Bytes() + } + + @Test + void testIntToBytesToInt() { + int iterations = 10000 + for (int i = 0; i < iterations; i++) { + int a = RANDOM.nextInt() + byte[] bytes = Bytes.toBytes(a) + int b = Bytes.toInt(bytes) + assertEquals a, b + } + } + + @Test + void testLongToBytesToLong() { + int iterations = 10000 + for (int i = 0; i < iterations; i++) { + long a = RANDOM.nextLong() + byte[] bytes = Bytes.toBytes(a) + long b = Bytes.toLong(bytes) + assertEquals a, b + } + } + + @Test + void testConcatNull() { + byte[] output = Bytes.concat(null) + assertNotNull output + assertEquals 0, output.length + } + + @Test + void testConcatSingle() { + byte[] bytes = new byte[32] + RANDOM.nextBytes(bytes) + byte[][] arg = [bytes] as byte[][] + byte[] output = Bytes.concat(arg) + assertTrue MessageDigest.isEqual(bytes, output) + } + + @Test + void testConcatSingleEmpty() { + byte[] bytes = new byte[0] + byte[][] arg = [bytes] as byte[][] + byte[] output = Bytes.concat(arg) + assertNotNull output + assertEquals 0, output.length + } + + @Test + void testConcatMultiple() { + byte[] a = new byte[32]; RANDOM.nextBytes(a) + byte[] b = new byte[16]; RANDOM.nextBytes(b) + + byte[] output = Bytes.concat(a, b) + + assertNotNull output + assertEquals a.length + b.length, output.length + + byte[] partA = new byte[a.length] + System.arraycopy(output, 0, partA, 0, a.length) + assertTrue MessageDigest.isEqual(a, partA) + + byte[] partB = new byte[b.length] + System.arraycopy(output, a.length, partB, 0, b.length) + assertTrue MessageDigest.isEqual(b, partB) + } + + @Test + void testConcatMultipleWithOneEmpty() { + + byte[] a = new byte[32]; RANDOM.nextBytes(a) + byte[] b = new byte[0] + + byte[] output = Bytes.concat(a, b) + + assertNotNull output + assertEquals a.length + b.length, output.length + + byte[] partA = new byte[a.length] + System.arraycopy(output, 0, partA, 0, a.length) + assertTrue MessageDigest.isEqual(a, partA) + + byte[] partB = new byte[b.length] + System.arraycopy(output, a.length, partB, 0, b.length) + assertTrue MessageDigest.isEqual(b, partB) + } + + @Test + void testLength() { + int len = 32 + assertEquals len, Bytes.length(new byte[len]) + } + + @Test + void testLengthZero() { + assertEquals 0, Bytes.length(new byte[0]) + } + + @Test + void testLengthNull() { + assertEquals 0, Bytes.length(null) + } + + @Test + void testBitLength() { + int len = 32 + byte[] a = new byte[len] + assertEquals len * Byte.SIZE, Bytes.bitLength(a) + } + + @Test + void testBitLengthZero() { + assertEquals 0, Bytes.bitLength(new byte[0]) + } + + @Test + void testBitLengthNull() { + assertEquals 0, Bytes.bitLength(null) + } + + @Test + void testIncrement() { + + byte[] counter = Bytes.toBytes(0) + for (int i = 0; i < 100; i++) { + assertEquals i, Bytes.toInt(counter) + Bytes.increment(counter) + } + + counter = Bytes.toBytes(Integer.MAX_VALUE - 1) + + Bytes.increment(counter) + assertEquals Integer.MAX_VALUE, Bytes.toInt(counter) + + //check correct integer overflow: + Bytes.increment(counter) + assertEquals Integer.MIN_VALUE, Bytes.toInt(counter) + } + + @Test + void testIncrementEmpty() { + byte[] counter = new byte[0] + Bytes.increment(counter) + assertTrue MessageDigest.isEqual(new byte[0], counter) + } + + @Test + void testIndexOfFromIndexOOB() { + int i = Bytes.indexOf(A, 0, A.length, B, 0, B.length, A.length) + assertEquals(-1, i) + } + + @Test + void testIndexOfFromIndexOOBWithZeroLengthTarget() { + int i = Bytes.indexOf(A, 0, A.length, B, 0, 0, A.length) + assertEquals(A.length, i) + } + + @Test + void testIndexOfFromIndexNegative() { + int i = Bytes.indexOf(A, 0, A.length, B, 0, B.length, -1) // should normalize fromIndex to be zero + assertEquals(2, i) // B starts at A index 2 + } + + @Test + void testIndexOfEmptyTargetIsZero() { + int i = Bytes.indexOf(A, Bytes.EMPTY) + assertEquals(0, i) + } + + @Test + void testIndexOfOOBSrcIndex() { + int i = Bytes.indexOf(A, 3, 2, B, 1, A.length, 0) + assertEquals(-1, i) + } + + @Test + void testIndexOfDisjointSrcAndTarget() { + int i = Bytes.indexOf(A, D) + assertEquals(-1, i) + } + + @Test + void testIndexOfPartialMatch() { + int i = Bytes.indexOf(A, C) + assertEquals(-1, i) + } + + @Test + void testIndexOfPartialMatchEndDifferent() { + byte[] toTest = [0x00, 0x01, 0x02, 0x03, 0x04, 0x06] // last byte is different in A + int i = Bytes.indexOf(A, toTest) + assertEquals(-1, i) + } + + @Test + void testStartsWith() { + byte[] A = [0x01, 0x02, 0x03] + byte[] B = [0x01, 0x03] + byte[] C = [0x02, 0x03] + assertTrue Bytes.startsWith(A, A, 0) + assertFalse Bytes.startsWith(A, B) + assertTrue Bytes.endsWith(A, C) + assertFalse Bytes.startsWith(A, A, -1) + assertFalse Bytes.startsWith(C, A) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy new file mode 100644 index 00000000..66f74693 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CollectionConverterTest.groovy @@ -0,0 +1,110 @@ +/* + * 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.lang + +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +class CollectionConverterTest { + + private static final UriStringConverter ELEMENT_CONVERTER = new UriStringConverter(); //any will do + + @Test + void testApplyToNull() { + assertNull Converters.forSet(ELEMENT_CONVERTER).applyTo(null) + assertNull Converters.forList(ELEMENT_CONVERTER).applyTo(null) + } + + @Test + void testApplyToEmpty() { + def set = [] as Set + assertSame set, Converters.forSet(ELEMENT_CONVERTER).applyTo(set) + def list = [] as List + assertSame list, Converters.forList(ELEMENT_CONVERTER).applyTo(list) + } + + @Test + void testApplyFromNull() { + assertNull Converters.forSet(ELEMENT_CONVERTER).applyFrom(null) + assertNull Converters.forList(ELEMENT_CONVERTER).applyFrom(null) + } + + @Test + void testApplyFromEmpty() { + def set = Converters.forSet(ELEMENT_CONVERTER).applyFrom([] as Set) + assertNotNull set + assertTrue set.isEmpty() + def list = Converters.forList(ELEMENT_CONVERTER).applyFrom([]) + assertNotNull list + assertTrue list.isEmpty() + } + + @Test + void testApplyFromNonPrimitiveArray() { + + String url = 'https://github.com/jwtk/jjwt' + URI uri = ELEMENT_CONVERTER.applyFrom(url) + def array = [url] as String[] + + def set = Converters.forSet(ELEMENT_CONVERTER).applyFrom(array) + assertNotNull set + assertEquals 1, set.size() + assertEquals uri, set.iterator().next() + + def list = Converters.forList(ELEMENT_CONVERTER).applyFrom(array) + assertNotNull list + assertEquals 1, list.size() + assertEquals uri, set.iterator().next() + } + + @Test + void testApplyFromPrimitiveArray() { + + // ensure the primitive array is not converted to a collection. That is, + // a byte array of length 4 should not return a collection of size 4. It should return a collection of size 1 + // and that element is the byte array + + Converter converter = new Converter() { + @Override + Object applyTo(String s) { + return Decoders.BASE64URL.decode(s); + } + + @Override + String applyFrom(Object o) { + return Encoders.BASE64URL.encode((byte[]) o); + } + } + + byte[] bytes = "1234".getBytes(StandardCharsets.UTF_8) + String s = converter.applyFrom(bytes) + + def set = Converters.forSet(converter).applyFrom(bytes) + assertNotNull set + assertEquals 1, set.size() + assertEquals s, set.iterator().next() + + def list = Converters.forList(converter).applyFrom(bytes) + assertNotNull list + assertEquals 1, list.size() + assertEquals s, set.iterator().next() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverterTest.groovy new file mode 100644 index 00000000..971b601b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/CompactMediaTypeIdConverterTest.groovy @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 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 + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class CompactMediaTypeIdConverterTest { + + private static final Converter converter = CompactMediaTypeIdConverter.INSTANCE + + @Test(expected = IllegalArgumentException) + void testApplyToNull() { + converter.applyTo(null) + } + + @Test(expected = IllegalArgumentException) + void testApplyToEmpty() { + converter.applyTo('') + } + + @Test(expected = IllegalArgumentException) + void testApplyToBlank() { + converter.applyTo(' ') + } + + @Test(expected = IllegalArgumentException) + void testApplyFromNull() { + converter.applyFrom(null) + } + + @Test(expected = IllegalArgumentException) + void testApplyFromNonString() { + converter.applyFrom(42) + } + + @Test + void testNonApplicationMediaType() { + String cty = 'foo' + assertEquals cty, converter.applyTo(cty) + assertEquals cty, converter.applyFrom(cty) + } + + @Test + void testApplicationMediaType() { + String cty = 'foo' + String mediaType = "application/$cty" + // assert it has been automatically compacted per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + assertEquals cty, converter.applyTo(mediaType) + } + + @Test + void testCaseInsensitiveApplicationMediaType() { // media type values are case insensitive + String cty = 'FoO' + String mediaType = "aPpLiCaTiOn/$cty" + // assert it has been automatically compacted per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + assertEquals cty, converter.applyTo(mediaType) + } + + @Test + void testApplicationMediaTypeWithMoreThanOneForwardSlash() { + String mediaType = "application/foo;part=1/2" + // cannot be compacted per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + assertEquals mediaType, converter.applyTo(mediaType) + } + + @Test + void testCaseInsensitiveApplicationMediaTypeWithMoreThanOneForwardSlash() { + String mediaType = "aPpLiCaTiOn/foo;part=1/2" + // cannot be compacted per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + assertEquals mediaType, converter.applyTo(mediaType) + } + + @Test + void testApplicationMediaTypeWithMoreThanOneForwardSlash2() { + String mediaType = "application//test" + // cannot be compacted per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + assertEquals mediaType, converter.applyTo(mediaType) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy new file mode 100644 index 00000000..f7e66683 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/ConvertersTest.groovy @@ -0,0 +1,26 @@ +/* + * 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.lang + +import org.junit.Test + +class ConvertersTest { + + @Test + void testPrivateCtor() { // only for code coverage + new Converters() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy new file mode 100644 index 00000000..e864396f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/EncodedObjectConverterTest.groovy @@ -0,0 +1,38 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.* + +class EncodedObjectConverterTest { + + @Test + void testApplyFromWithInvalidType() { + def converter = Converters.URI + assertTrue converter instanceof EncodedObjectConverter + int value = 42 + try { + converter.applyFrom(value) + fail("IllegalArgumentException should have been thrown.") + } catch (IllegalArgumentException expected) { + String msg = "Values must be either String or java.net.URI instances. " + + "Value type found: java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy new file mode 100644 index 00000000..ef1869e1 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy @@ -0,0 +1,224 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.* + +class FieldsTest { + + static final Field STRING = Fields.builder(String.class).setId('foo').setName('FooName').build() + static final Field> STRSET = Fields.builder(String.class).setId('fooSet').setName('FooSet').set().build() + static final Field> STRLIST = Fields.builder(String.class).setId('fooList').setName('FooList').list().build() + + @Test + void testPrivateCtor() { // for code coverage only + new Fields() + } + + @Test + void testString() { + assertEquals 'FooName', STRING.getName() + assertEquals 'foo', STRING.getId() + } + + @Test + void testSupports() { + assertTrue STRING.supports('bar') // any string + } + + @Test + void testSupportsNull() { + assertTrue STRING.supports(null) // null values allowed by default + } + + @Test + void testSupportsSet() { + def set = ['test'] as Set + assertTrue STRSET.supports(set) + } + + @Test + void testSupportsSetNull() { + assertTrue STRSET.supports(null) + } + + @Test + void testSupportsSetEmpty() { + def set = [] as Set + assertTrue STRSET.supports(set) + } + + @Test + void testSupportsSetWrongElementType() { + def set = [42] as Set // correct collection type, but wrong element type + assertFalse STRSET.supports(set) + } + + @Test + void testSupportsList() { + def list = ['test'] + assertTrue STRLIST.supports(list) + } + + @Test + void testSupportsListNull() { + assertTrue STRLIST.supports(null) + } + + @Test + void testSupportsListEmpty() { + def list = [] as List + assertTrue STRLIST.supports(list) + } + + @Test + void testSupportsListWrongElementType() { + def list = [42] // correct collection type, but wrong element type + assertFalse STRLIST.supports(list) + } + + @Test + void testSupportsFailsForDifferentType() { + def field = Fields.builder(String.class).setId('foo').setName('fooName').build() + Object val = 42 + assertFalse field.supports(val) + } + + @Test + void testCast() { + Object val = 'test' + assertEquals val, STRING.cast(val) + } + + @Test + void testCastNull() { + assertNull STRING.cast(null) + assertNull STRSET.cast(null) + assertNull STRLIST.cast(null) + } + + @Test + void testCastWrongType() { + try { + STRING.cast(42) + fail() + } catch (ClassCastException expected) { + String msg = 'Cannot cast java.lang.Integer to java.lang.String' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testCastSet() { + Object set = ['test'] as Set + assertSame set, STRSET.cast(set) + } + + @Test + void testCastSetEmpty() { + Object set = [] as Set + assertSame set, STRSET.cast(set) + } + + @Test + void testCastSetWrongType() { + try { + STRSET.cast(42) // not a set + fail() + } catch (ClassCastException expected) { + String msg = "Cannot cast java.lang.Integer to java.util.Set" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testCastSetWrongElementType() { + Object set = [42] as Set + try { + STRSET.cast(set) + fail() + } catch (ClassCastException expected) { + String msg = "Cannot cast java.util.LinkedHashSet to java.util.Set: At least " + + "one element is not an instance of java.lang.String" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testCastList() { + Object list = ['test'] + assertSame list, STRLIST.cast(list) + } + + @Test + void testCastListEmpty() { + Object list = [] + assertSame list, STRLIST.cast(list) + } + + @Test + void testCastListWrongType() { + try { + STRLIST.cast(42) // not a list + fail() + } catch (ClassCastException expected) { + String msg = "Cannot cast java.lang.Integer to java.util.List" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testCastListWrongElementType() { + Object list = [42] + try { + STRLIST.cast(list) + fail() + } catch (ClassCastException expected) { + String msg = "Cannot cast java.util.ArrayList to java.util.List: At least " + + "one element is not an instance of java.lang.String" + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEquals() { + def a = Fields.string('foo', "NameA") + def b = Fields.builder(Object.class).setId('foo').setName("NameB").build() + //ensure equality only based on id: + assertEquals a, b + } + + @Test + void testHashCode() { + def a = Fields.string('foo', "NameA") + def b = Fields.builder(Object.class).setId('foo').setName("NameB").build() + //ensure only based on id: + assertEquals a.hashCode(), b.hashCode() + } + + @Test + void testToString() { + assertEquals "'foo' (FooName)", Fields.string('foo', 'FooName').toString() + } + + @Test + void testEqualsNonField() { + def field = Fields.builder(String.class).setId('foo').setName("FooName").build() + assertFalse field.equals(new Object()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy new file mode 100644 index 00000000..09c8b657 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FunctionsTest.groovy @@ -0,0 +1,222 @@ +/* + * 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.lang + +import io.jsonwebtoken.MalformedJwtException +import org.junit.Test + +import static org.junit.Assert.* + +class FunctionsTest { + + @Test + void testWrapFmt() { + + def cause = new IllegalStateException("foo") + + def fn = Functions.wrapFmt(new CheckedFunction() { + @Override + Object apply(Object o) throws Exception { + throw cause + } + }, MalformedJwtException, "format me %s") + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + String msg = "format me hi. Cause: foo" + assertEquals msg, expected.getMessage() + assertSame cause, expected.getCause() + } + } + + @Test + void testWrapFmtPropagatesExpectedExceptionTypeWithoutWrapping() { + + def cause = new MalformedJwtException("foo") + + def fn = Functions.wrapFmt(new CheckedFunction() { + @Override + Object apply(Object o) throws Exception { + throw cause + } + }, MalformedJwtException, "format me %s") + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + assertEquals "foo", expected.getMessage() + assertSame cause, expected + } + } + + @Test + void testWrap() { + + def cause = new IllegalStateException("foo") + + def fn = Functions.wrap(new Function() { + @Override + Object apply(Object o) { + throw cause + } + }, MalformedJwtException, "format me %s", 'someArg') + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + String msg = "format me someArg. Cause: foo" + assertEquals msg, expected.getMessage() + assertSame cause, expected.getCause() + } + } + + @Test + void testWrapPropagatesExpectedExceptionTypeWithoutWrapping() { + + def cause = new MalformedJwtException("foo") + + def fn = Functions.wrap(new Function() { + @Override + Object apply(Object o) { + throw cause + } + }, MalformedJwtException, "format me %s", 'someArg') + + try { + fn.apply('hi') + fail() + } catch (MalformedJwtException expected) { + assertEquals "foo", expected.getMessage() + assertSame cause, expected + } + } + + @Test + void testFirstResultWithNullArgument() { + try { + Functions.firstResult(null) + fail() + } catch (IllegalArgumentException iae) { + assertEquals 'Function list cannot be null or empty.', iae.getMessage() + } + } + + @Test + void testFirstResultWithEmptyArgument() { + Function[] functions = [] as Function[] + try { + Functions.firstResult(functions) + fail() + } catch (IllegalArgumentException iae) { + assertEquals 'Function list cannot be null or empty.', iae.getMessage() + } + } + + @Test + void testFirstResultWithSingleNonNullValueFunction() { + Function fn = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s + return s + } + } + assertEquals 'foo', Functions.firstResult(fn).apply('foo') + } + + @Test + void testFirstResultWithSingleNullValueFunction() { + Function fn = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s + return null + } + } + assertNull Functions.firstResult(fn).apply('foo') + } + + @Test + void testFirstResultFallback() { + def fn1 = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s + return null + } + } + def fn2 = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s // ensure original input is retained, not output from fn1 + return 'fn2' + } + } + assertEquals 'fn2', Functions.firstResult(fn1, fn2).apply('foo') + } + + @Test + void testFirstResultAllNull() { + def fn1 = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s + return null + } + } + def fn2 = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s // ensure original input is retained, not output from fn1 + return null + } + } + // everything returned null, so null should be returned: + assertNull Functions.firstResult(fn1, fn2).apply('foo') + } + + @Test + void testFirstResultShortCircuit() { + def fn1 = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s + return null + } + } + def fn2 = new Function() { + @Override + String apply(String s) { + assertEquals 'foo', s // ensure original argument is retained, not output from fn1 + return 'fn2' + } + } + boolean invoked = false + def fn3 = new Function() { + @Override + String apply(String s) { + invoked = true // should not be invoked + return 'fn3' + } + } + assertEquals 'fn2', Functions.firstResult(fn1, fn2, fn3).apply('foo') + assertFalse invoked + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy new file mode 100644 index 00000000..bd25bdcb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy @@ -0,0 +1,38 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.assertNull + +class JwtDateConverterTest { + + @Test + void testApplyToNull() { + assertNull JwtDateConverter.INSTANCE.applyTo(null) + } + + @Test + void testApplyFromNull() { + assertNull JwtDateConverter.INSTANCE.applyFrom(null) + } + + @Test + void testToDateWithNull() { + assertNull JwtDateConverter.toDate(null) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LegacyServicesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LegacyServicesTest.groovy index 60cd3cb3..264e43c2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LegacyServicesTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LegacyServicesTest.groovy @@ -1,3 +1,18 @@ +/* + * 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.lang import io.jsonwebtoken.lang.UnknownClassException diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy new file mode 100644 index 00000000..410661bd --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/LocatorFunctionTest.groovy @@ -0,0 +1,45 @@ +/* + * 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.lang + +import io.jsonwebtoken.Header +import io.jsonwebtoken.Locator +import io.jsonwebtoken.impl.DefaultJweHeader +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class LocatorFunctionTest { + + @Test + void testApply() { + final int value = 42 + def locator = new StaticLocator(value) + def fn = new LocatorFunction(locator) + assertEquals value, fn.apply(new DefaultJweHeader()) + } + + static class StaticLocator implements Locator { + private final T o; + StaticLocator(T o) { + this.o = o; + } + @Override + T locate(Header header) { + return o; + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy new file mode 100644 index 00000000..d7afacda --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/NullSafeConverterTest.groovy @@ -0,0 +1,40 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNull + +class NullSafeConverterTest { + + @Test + void testNullArguments() { + def converter = new NullSafeConverter(new UriStringConverter()) + assertNull converter.applyTo(null) + assertNull converter.applyFrom(null) + } + + @Test + void testNonNullArguments() { + def converter = new NullSafeConverter(new UriStringConverter()) + String url = 'https://github.com/jwtk/jjwt' + URI uri = new URI(url) + assertEquals url, converter.applyTo(uri) + assertEquals uri, converter.applyFrom(url) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/OptionalCtorInvokerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/OptionalCtorInvokerTest.groovy new file mode 100644 index 00000000..57022791 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/OptionalCtorInvokerTest.groovy @@ -0,0 +1,68 @@ +/* + * 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 + +import org.junit.Test + +import javax.crypto.spec.PBEKeySpec + +import static org.junit.Assert.* + +class OptionalCtorInvokerTest { + + @Test + void testCtorWithClassArg() { + String foo = 'test' + def fn = new OptionalCtorInvoker<>("java.lang.String", String.class) // copy constructor + def result = fn.apply(foo) + assertEquals foo, result + } + + @Test + void testCtorWithFqcnArg() { + String foo = 'test' + def fn = new OptionalCtorInvoker<>("java.lang.String", "java.lang.String") // copy constructor + def result = fn.apply(foo) + assertEquals foo, result + } + + @Test + void testCtorWithMultipleMixedArgTypes() { + char[] chars = "foo".toCharArray() + byte[] salt = [0x00, 0x01, 0x02, 0x03] as byte[] + int iterations = 256 + def fn = new OptionalCtorInvoker<>("javax.crypto.spec.PBEKeySpec", char[].class, byte[].class, int.class) //password, salt, iteration count + def args = [chars, salt, iterations] as Object[] + def result = fn.apply(args) as PBEKeySpec + assertArrayEquals chars, result.getPassword() + assertArrayEquals salt, result.getSalt() + assertEquals iterations, result.getIterationCount() + } + + @Test + void testZeroArgConstructor() { + OptionalCtorInvoker fn = new OptionalCtorInvoker("java.util.LinkedHashMap") + Object args = null + def result = fn.apply(args) + assertTrue result instanceof LinkedHashMap + } + + @Test + void testMissingConstructor() { + def fn = new OptionalCtorInvoker('com.foo.Bar') + assertNull fn.apply(null) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/OptionalMethodInvokerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/OptionalMethodInvokerTest.groovy new file mode 100644 index 00000000..86eb03fa --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/OptionalMethodInvokerTest.groovy @@ -0,0 +1,72 @@ +/* + * 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 + +import io.jsonwebtoken.impl.security.TestKeys +import org.junit.Test + +import java.lang.reflect.InvocationTargetException +import java.security.Key + +import static org.junit.Assert.* + +class OptionalMethodInvokerTest { + + @Test + void testClassDoesNotExist() { + def i = new OptionalMethodInvoker('com.foo.Bar', 'foo') + assertNull i.apply(null) + } + + @Test + void testClassExistsButMethodDoesNotExist() { + def i = new OptionalMethodInvoker(Key.class.getName(), 'foo') + assertNull i.apply(null) + } + + @Test + void testClassAndMethodExistWithValidArgument() { + def key = TestKeys.HS256 + def i = new OptionalMethodInvoker(Key.class.getName(), 'getAlgorithm') + assertEquals key.getAlgorithm(), i.apply(key) + } + + @Test + void testClassAndMethodExistWithInvalidTypeArgument() { + def i = new OptionalMethodInvoker(Key.class.getName(), 'getAlgorithm') + assertNull i.apply('Hello') // not a Key instance, should return null + } + + @Test + void testClassAndMethodExistWithInvocationError() { + def key = TestKeys.HS256 + String msg = 'bar' + def ex = new InvocationTargetException(new IllegalStateException('foo'), msg) + def i = new OptionalMethodInvoker(Key.class.getName(), 'getEncoded') { + @Override + protected String invoke(Key aKey) throws InvocationTargetException, IllegalAccessException { + throw ex + } + } + try { + i.apply(key) // getEncoded returns a byte array, not a String, should throw cast error + fail() + } catch (IllegalStateException ise) { + assertEquals ReflectionFunction.ERR_MSG + msg, ise.getMessage() + assertSame ex, ise.getCause() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy new file mode 100644 index 00000000..b4d54cf2 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/PropagatingExceptionFunctionTest.groovy @@ -0,0 +1,82 @@ +/* + * 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.lang + +import io.jsonwebtoken.security.SecurityException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class PropagatingExceptionFunctionTest { + + @Test + void testAssignableException() { + + def ex = new SecurityException("test") + + def fn = new PropagatingExceptionFunction<>(new Function() { + @Override + Object apply(Object t) { + throw ex + } + }, SecurityException.class, "foo") + + try { + fn.apply("hi") + } catch (Exception thrown) { + assertSame ex, thrown //because it was assignable, 'thrown' should not be a wrapper exception + } + } + + @Test + void testExceptionMessageWithTrailingPeriod() { + String msg = 'foo.' + def ex = new IllegalArgumentException("test") + def fn = new PropagatingExceptionFunction<>(new Function() { + @Override + Object apply(Object t) { + throw ex + } + }, SecurityException.class, msg) + + try { + fn.apply("hi") + } catch (SecurityException expected) { + String expectedMsg ="$msg Cause: test" // expect $msg unaltered + assertEquals expectedMsg, expected.getMessage() + } + } + + @Test + void testExceptionMessageWithoutTrailingPeriod() { + String msg = 'foo' + def ex = new IllegalArgumentException("test") + def fn = new PropagatingExceptionFunction<>(new Function() { + @Override + Object apply(Object t) { + throw ex + } + }, SecurityException.class, msg) + + try { + fn.apply("hi") + } catch (SecurityException expected) { + String expectedMsg ="$msg. Cause: test" // expect $msg to have a trailing period + assertEquals expectedMsg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy new file mode 100644 index 00000000..a4686e51 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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 + +import org.junit.Test + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +class RedactedSupplierTest { + + @Test + void testEqualsWrappedSameValue() { + def value = 42 + assertTrue new RedactedSupplier<>(value).equals(value) + } + + @Test + void testEqualsWrappedDifferentValue() { + assertFalse new RedactedSupplier<>(42).equals(30) + } + + @Test + void testEquals() { + assertTrue new RedactedSupplier<>(42).equals(new RedactedSupplier(42)) + } + + @Test + void testEqualsSameTypeDifferentValue() { + assertFalse new RedactedSupplier<>(42).equals(new RedactedSupplier(30)) + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy new file mode 100644 index 00000000..65c776b6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedValueConverterTest.groovy @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 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 + +import org.junit.Test + +import static org.junit.Assert.assertNull +import static org.junit.Assert.assertSame + +class RedactedValueConverterTest { + + @Test + void testApplyToWithNullValue() { + def c = new RedactedValueConverter(new NullSafeConverter(Converters.URI)) + assertNull c.applyTo(null) + } + + @Test + void testApplyFromWithNullValue() { + def c = new RedactedValueConverter(new NullSafeConverter(Converters.URI)) + assertNull c.applyFrom(null) + } + + @Test + void testDelegateReturnsRedactedSupplierValue() { + def suri = 'https://jsonwebtoken.io' + def supplier = new RedactedSupplier(suri) + def delegate = new Converter() { + @Override + Object applyTo(Object o) { + return supplier + } + + @Override + Object applyFrom(Object o) { + return null + } + } + def c = new RedactedValueConverter(delegate) + + // ensure applyTo doesn't change or wrap the delegate return value that is already of type RedactedSupplier: + assertSame supplier, c.applyTo(suri) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy new file mode 100644 index 00000000..78176683 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RequiredTypeConverterTest.groovy @@ -0,0 +1,50 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class RequiredTypeConverterTest { + + @Test + void testApplyTo() { + def converter = new RequiredTypeConverter(Integer.class) + def val = 42 + assertSame val, converter.applyTo(val) + } + + @Test + void testApplyFromNull() { + def converter = new RequiredTypeConverter(Integer.class) + assertNull converter.applyFrom(null) + } + + @Test + void testApplyFromInvalidType() { + def converter = new RequiredTypeConverter(Integer.class) + try { + converter.applyFrom('hello' as String) + } catch (IllegalArgumentException expected) { + String msg = 'Unsupported value type. Expected: java.lang.Integer, found: java.lang.String' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy new file mode 100644 index 00000000..e478d1e1 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/UriStringConverterTest.groovy @@ -0,0 +1,43 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class UriStringConverterTest { + + @Test + void testApplyTo() { + String url = 'https://github.com/jwtk/jjwt' + URI uri = new URI(url) + def converter = new UriStringConverter() + assertEquals url, converter.applyTo(uri) + assertEquals uri, converter.applyFrom(url) + } + + @Test + void testApplyFromWithInvalidArgument() { + String val = '{}asdfasdfasd' + try { + new UriStringConverter().applyFrom(val) + } catch (IllegalArgumentException expected) { + String msg = "Unable to convert String value '${val}' to URI instance: Illegal character in path at index 0: ${val}" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy new file mode 100644 index 00000000..b8ddfedf --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractAsymmetricJwkBuilderTest.groovy @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.* +import org.junit.Test + +import java.security.cert.X509Certificate +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +class AbstractAsymmetricJwkBuilderTest { + + private static final X509Certificate CERT = TestKeys.RS256.cert + private static final List CHAIN = [CERT] + private static final RSAPublicKey PUB_KEY = CERT.getPublicKey() as RSAPublicKey + + private static RsaPublicJwkBuilder builder() { + return Jwks.builder().forKey(PUB_KEY) + } + + @Test + void testUse() { + def val = UUID.randomUUID().toString() + def jwk = builder().setPublicKeyUse(val).build() + assertEquals val, jwk.getPublicKeyUse() + assertEquals val, jwk.use + + RSAPrivateKey privateKey = TestKeys.RS256.pair.private as RSAPrivateKey + + jwk = builder().setPublicKeyUse(val).setPrivateKey(privateKey).build() + assertEquals val, jwk.getPublicKeyUse() + assertEquals val, jwk.use + } + + @Test + void testX509Url() { + def val = new URI(UUID.randomUUID().toString()) + assertSame val, builder().setX509Url(val).build().getX509Url() + } + + @Test + void testX509CertificateChain() { + assertEquals CHAIN, builder().setX509CertificateChain(CHAIN).build().getX509CertificateChain() + } + + @Test + void testX509CertificateSha1Thumbprint() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5t = DefaultHashAlgorithm.SHA1.digest(request) + def encoded = Encoders.BASE64URL.encode(x5t) + def jwk = builder().setX509CertificateSha1Thumbprint(x5t).build() + assertArrayEquals x5t, jwk.getX509CertificateSha1Thumbprint() + assertEquals encoded, jwk.get(AbstractAsymmetricJwk.X5T.getId()) + } + + @Test + void testX509CertificateSha1ThumbprintEnabled() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5t = DefaultHashAlgorithm.SHA1.digest(request) + def encoded = Encoders.BASE64URL.encode(x5t) + def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha1Thumbprint(true).build() + assertArrayEquals x5t, jwk.getX509CertificateSha1Thumbprint() + assertEquals encoded, jwk.get(AbstractAsymmetricJwk.X5T.getId()) + } + + @Test + void testX509CertificateSha256Thumbprint() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5tS256 = StandardHashAlgorithms.get().SHA256.digest(request) + def encoded = Encoders.BASE64URL.encode(x5tS256) + def jwk = builder().setX509CertificateSha256Thumbprint(x5tS256).build() + assertArrayEquals x5tS256, jwk.getX509CertificateSha256Thumbprint() + assertEquals encoded, jwk.get(AbstractAsymmetricJwk.X5T_S256.getId()) + } + + @Test + void testX509CertificateSha256ThumbprintEnabled() { + Request request = new DefaultRequest(TestKeys.RS256.cert.getEncoded(), null, null) + def x5tS256 = StandardHashAlgorithms.get().SHA256.digest(request) + def encoded = Encoders.BASE64URL.encode(x5tS256) + def jwk = builder().setX509CertificateChain(CHAIN).withX509Sha256Thumbprint(true).build() + assertArrayEquals x5tS256, jwk.getX509CertificateSha256Thumbprint() + assertEquals encoded, jwk.get(AbstractAsymmetricJwk.X5T_S256.getId()) + } + + @Test + void testEcPrivateJwkFromPublicBuilder() { + def pair = TestKeys.ES256.pair + + //start with a public key builder + def builder = Jwks.builder().forKey(pair.public as ECPublicKey) + assertTrue builder instanceof AbstractAsymmetricJwkBuilder.DefaultEcPublicJwkBuilder + + //applying the private key turns it into a private key builder + builder = builder.setPrivateKey(pair.private as ECPrivateKey) + assertTrue builder instanceof AbstractAsymmetricJwkBuilder.DefaultEcPrivateJwkBuilder + + //building creates a private jwk: + def jwk = builder.build() + assertTrue jwk instanceof EcPrivateJwk + + //which also has information for the public key: + jwk = jwk.toPublicJwk() + assertTrue jwk instanceof EcPublicJwk + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy new file mode 100644 index 00000000..835fffd6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy @@ -0,0 +1,134 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.KeyFactory +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class AbstractEcJwkFactoryTest { + + @Test + void testInvalidJwaCurveId() { + String id = 'foo' + try { + AbstractEcJwkFactory.getCurveByJwaId(id) + fail() + } catch (UnsupportedKeyException e) { + String msg = "Unrecognized JWA curve id '$id'" + assertEquals msg, e.getMessage() + } + } + + @Test + void testUnsupportedCurve() { + def curve = new EllipticCurve(new TestECField(fieldSize: 1), BigInteger.ONE, BigInteger.TEN) + try { + AbstractEcJwkFactory.getJwaIdByCurve(curve) + fail() + } catch (UnsupportedKeyException e) { + assertEquals AbstractEcJwkFactory.UNSUPPORTED_CURVE_MSG, e.getMessage() + } + } + + @Test + void testMultiplyInfinity() { + ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') + def result = AbstractEcJwkFactory.multiply(ECPoint.POINT_INFINITY, BigInteger.valueOf(1), spec) + assertEquals ECPoint.POINT_INFINITY, result + } + + @Test + void testDoubleInfinity() { + ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') + def curve = spec.getCurve() + def result = AbstractEcJwkFactory.doublePoint(ECPoint.POINT_INFINITY, curve) + assertEquals ECPoint.POINT_INFINITY, result + } + + @Test + void testAddInfinity() { + ECParameterSpec spec = AbstractEcJwkFactory.getCurveByJwaId('P-256') + def curve = spec.getCurve() + ECPoint point = new ECPoint(BigInteger.valueOf(1), BigInteger.valueOf(2)) // any point is fine for this test + def result = AbstractEcJwkFactory.add(ECPoint.POINT_INFINITY, point, curve) + //adding infinity to a point should return the point: + assertEquals point, result + //adding a point to infinity should return the point: + result = AbstractEcJwkFactory.add(point, ECPoint.POINT_INFINITY, curve) + assertEquals point, result + } + + @Test + void testAddSamePointDoublesIt() { + def pair = Jwts.SIG.ES256.keyPairBuilder().build() + def pub = pair.getPublic() as ECPublicKey + + def spec = pub.getParams() + def curve = spec.getCurve() + def point = pub.getW() + + def doubled = AbstractEcJwkFactory.doublePoint(point, curve) + def added = AbstractEcJwkFactory.add(point, point, curve) + assertEquals doubled, added + } + + @Test + void testDerivePublicFails() { + + def pair = Jwts.SIG.ES256.keyPairBuilder().build() + def priv = pair.getPrivate() as ECPrivateKey + + final def context = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) + context.setKey(priv) + + def ex = new InvalidKeySpecException("invalid") + + def factory = new AbstractEcJwkFactory(ECPrivateKey.class, DefaultEcPrivateJwk.FIELDS) { + @Override + protected Jwk createJwkFromKey(JwkContext ctx) { + return null + } + + @Override + protected Jwk createJwkFromValues(JwkContext ctx) { + return null + } + + @Override + protected ECPublicKey derivePublic(KeyFactory keyFactory, ECPublicKeySpec spec) throws InvalidKeySpecException { + throw ex + } + } + + try { + factory.derivePublic(context) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Unable to derive ECPublicKey from ECPrivateKey: invalid' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy new file mode 100644 index 00000000..1759bd59 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactoryTest.groovy @@ -0,0 +1,86 @@ +/* + * 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.security + +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.KeyException +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import java.security.KeyFactory +import java.security.NoSuchAlgorithmException +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.* + +class AbstractFamilyJwkFactoryTest { + + @Test + void testGenerateKeyPropagatesKeyException() { + // any AbstractFamilyJwkFactory subclass will do: + def factory = new EcPublicJwkFactory() + def ctx = new DefaultJwkContext() + ctx.put('hello', 'world') + def ex = new MalformedKeyException('foo') + try { + factory.generateKey(ctx, new CheckedFunction() { + @Override + ECPublicKey apply(KeyFactory keyFactory) throws Exception { + throw ex + } + }) + fail() + } catch (KeyException expected) { + assertSame ex, expected + } + } + + @Test + void testGenerateKeyUnexpectedException() { + // any AbstractFamilyJwkFactory subclass will do: + def factory = new EcPublicJwkFactory() + def ctx = new DefaultJwkContext() + ctx.put('hello', 'world') + try { + factory.generateKey(ctx, new CheckedFunction() { + @Override + ECPublicKey apply(KeyFactory keyFactory) throws Exception { + throw new NoSuchAlgorithmException("foo") + } + }) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'Unable to create ECPublicKey from JWK {hello=world}: foo', expected.getMessage() + } + } + + @Test + void testUnsupportedContext() { + def factory = new EcPublicJwkFactory() { + @Override + boolean supports(JwkContext ctx) { + return false + } + } + try { + factory.createJwk(new DefaultJwkContext()) + fail() + } catch (IllegalArgumentException iae) { + assertEquals 'Unsupported JwkContext.', iae.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy new file mode 100644 index 00000000..84bc1024 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -0,0 +1,165 @@ +/* + * 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.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.SecretJwk +import org.junit.Test + +import javax.crypto.SecretKey +import java.security.Key + +import static org.junit.Assert.* + +class AbstractJwkBuilderTest { + + private static final SecretKey SKEY = TestKeys.A256GCM + + private static AbstractJwkBuilder builder() { + return (AbstractJwkBuilder) Jwks.builder().forKey(SKEY) + } + + @Test + void testKeyType() { + def jwk = builder().build() + assertEquals 'oct', jwk.getType() + assertNotNull jwk.k // JWA id for raw key value + } + + @Test + void testPut() { + def a = UUID.randomUUID() + def builder = builder() + builder.put('foo', a) + assertEquals a, builder.build().get('foo') + } + + @Test + void testPutAll() { + def foo = UUID.randomUUID() + def bar = UUID.randomUUID().toString() //different type + def m = [foo: foo, bar: bar] + def jwk = builder().putAll(m).build() + assertEquals foo, jwk.foo + assertEquals bar, jwk.bar + } + + @Test + void testRemove() { + def jwk = builder().put('foo', 'bar').remove('foo').build() as Jwk + assertNull jwk.get('foo') + } + + @Test + void testClear() { + def jwk = builder().put('foo', 'bar').clear().build() as Jwk + assertNull jwk.get('foo') + } + + @Test + void testAlgorithm() { + def alg = 'someAlgorithm' + def jwk = builder().setAlgorithm(alg).build() + assertEquals alg, jwk.getAlgorithm() + assertEquals alg, jwk.alg //test raw get via JWA member id + } + + @Test + void testAlgorithmByPut() { + def alg = 'someAlgorithm' + def jwk = builder().put('alg', alg).build() //ensure direct put still is handled properly + assertEquals alg, jwk.getAlgorithm() + assertEquals alg, jwk.alg //test raw get via JWA member id + } + + @Test + void testId() { + def kid = UUID.randomUUID().toString() + def jwk = builder().setId(kid).build() + assertEquals kid, jwk.getId() + assertEquals kid, jwk.kid //test raw get via JWA member id + } + + @Test + void testIdByPut() { + def kid = UUID.randomUUID().toString() + def jwk = builder().put('kid', kid).build() + assertEquals kid, jwk.getId() + assertEquals kid, jwk.kid //test raw get via JWA member id + } + + @Test + void testOperations() { + def a = UUID.randomUUID().toString() + def b = UUID.randomUUID().toString() + def set = [a, b] as Set + def jwk = builder().setOperations(set).build() + assertEquals set, jwk.getOperations() + assertEquals set, jwk.key_ops + } + + @Test + void testOperationsByPut() { + def a = UUID.randomUUID().toString() + def b = UUID.randomUUID().toString() + def set = [a, b] as Set + def jwk = builder().put('key_ops', set).build() + assertEquals set, jwk.getOperations() + assertEquals set, jwk.key_ops + } + + @Test + //ensures that even if a raw single value is present it is represented as a Set per the JWA spec (string array) + void testOperationsByPutSingleValue() { + def a = UUID.randomUUID().toString() + def set = [a] as Set + def jwk = builder().put('key_ops', a).build() // <-- put uses single raw value, not a set + assertEquals set, jwk.getOperations() // <-- still get a set + assertEquals set, jwk.key_ops // <-- still get a set + } + + @Test + void testProvider() { + def provider = Providers.findBouncyCastle(Conditions.TRUE) + def jwk = builder().setProvider(provider).build() + assertEquals 'oct', jwk.getType() + } + + @Test + void testFactoryThrowsIllegalArgumentException() { + def ctx = new DefaultJwkContext() + ctx.put('whatevs', 42) + //noinspection GroovyUnusedAssignment + JwkFactory factory = new JwkFactory() { + JwkContext newContext(JwkContext src, Key key) { + return null + } + @Override + Jwk createJwk(JwkContext jwkContext) { + throw new IllegalArgumentException("foo") + } + } + def builder = new AbstractJwkBuilder(ctx, factory) {} + try { + builder.build() + } catch (MalformedKeyException expected) { + assertEquals 'Unable to create JWK: foo', expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy new file mode 100644 index 00000000..dcf4fb59 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy @@ -0,0 +1,167 @@ +/* + * 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.security + +import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.Jwks +import org.junit.Before +import org.junit.Test + +import javax.crypto.SecretKey +import java.security.Key + +import static org.junit.Assert.* + +class AbstractJwkTest { + + AbstractJwk jwk + + static JwkContext newCtx() { + return newCtx(null) + } + + static JwkContext newCtx(Map map) { + def ctx = new DefaultJwkContext(AbstractJwk.FIELDS) + ctx.put('kty', 'test') + if (!Collections.isEmpty(map as Map)) { + ctx.putAll(map) + } + ctx.setKey(TestKeys.HS256) + return ctx + } + + static AbstractJwk newJwk(JwkContext ctx) { + return new AbstractJwk(ctx, Collections.of(AbstractJwk.KTY)) {} + } + + @Before + void setUp() { + jwk = newJwk(newCtx()) + } + + @Test + void testGetFieldValue() { + assertEquals 'test', jwk.get(AbstractJwk.KTY) + } + + @Test + void testContainsValue() { + assertTrue jwk.containsValue('test') + assertFalse jwk.containsValue('bar') + } + + static void jwkImmutable(Closure c) { + try { + c.call() + fail() + } catch (UnsupportedOperationException expected) { + String msg = 'JWKs are immutable and may not be modified.' + assertEquals msg, expected.getMessage() + } + } + + static void jucImmutable(Closure c) { + try { + c.call() + fail() + } catch (UnsupportedOperationException expected) { + assertNull expected.getMessage() // java.util.Collections.unmodifiable* doesn't give a message + } + } + + @Test + void testImmutable() { + jwk = newJwk(newCtx()) + jwkImmutable { jwk.put('foo', 'bar') } + jwkImmutable { jwk.putAll([foo: 'bar']) } + jwkImmutable { jwk.remove('kty') } + jwkImmutable { jwk.clear() } + } + + @Test + // ensure that any map or collection returned from the JWK is immutable as well: + void testCollectionsAreImmutable() { + def vals = [ + map : [foo: 'bar'], + list : ['a'], + set : ['b'] as Set, + collection: ['c'] as Collection + ] + jwk = newJwk(newCtx(vals)) + jucImmutable { (jwk.get('map') as Map).remove('foo') } + jucImmutable { (jwk.get('list') as List).remove(0) } + jucImmutable { (jwk.get('set') as Set).remove('b') } + jucImmutable { (jwk.get('collection') as Collection).remove('c') } + jucImmutable { jwk.keySet().remove('map') } + jucImmutable { jwk.values().remove('a') } + } + + @Test + // ensure that any array value returned from the JWK is a copy, so modifying it won't modify the original array + void testArraysAreCopied() { + def vals = [ + array: ['a', 'b'] as String[] + ] + jwk = newJwk(newCtx(vals)) + def returned = jwk.get('array') + assertTrue returned instanceof String[] + assertEquals 2, returned.length + + //now modify it: + returned[0] = 'x' + + //ensure the array structure hasn't changed: + def returned2 = jwk.get('array') + assertEquals 'a', returned2[0] + assertEquals 'b', returned2[1] + } + + @Test + void testPrivateJwkToStringHasRedactedValues() { + def secretJwk = Jwks.builder().forKey(TestKeys.HS256).build() + assertTrue secretJwk.toString().contains('k=') + + def ecPrivJwk = Jwks.builder().forKey(TestKeys.ES256.pair.private).build() + assertTrue ecPrivJwk.toString().contains('d=') + + def rsaPrivJwk = Jwks.builder().forKey(TestKeys.RS256.pair.private).build() + String s = 'd=, p=, q=, dp=, dq=, qi=' + assertTrue rsaPrivJwk.toString().contains(s) + } + + @Test + void testPrivateJwkHashCode() { + assertEquals jwk.hashCode(), jwk.@context.hashCode() + + def secretJwk1 = Jwks.builder().forKey(TestKeys.HS256).put('hello', 'world').build() + def secretJwk2 = Jwks.builder().forKey(TestKeys.HS256).put('hello', 'world').build() + assertEquals secretJwk1.hashCode(), secretJwk1.@context.hashCode() + assertEquals secretJwk2.hashCode(), secretJwk2.@context.hashCode() + assertEquals secretJwk1.hashCode(), secretJwk2.hashCode() + + def ecPrivJwk1 = Jwks.builder().forKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() + def ecPrivJwk2 = Jwks.builder().forKey(TestKeys.ES256.pair.private).put('hello', 'ecworld').build() + assertEquals ecPrivJwk1.hashCode(), ecPrivJwk2.hashCode() + assertEquals ecPrivJwk1.hashCode(), ecPrivJwk1.@context.hashCode() + assertEquals ecPrivJwk2.hashCode(), ecPrivJwk2.@context.hashCode() + + def rsaPrivJwk1 = Jwks.builder().forKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() + def rsaPrivJwk2 = Jwks.builder().forKey(TestKeys.RS256.pair.private).put('hello', 'rsaworld').build() + assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk2.hashCode() + assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk1.@context.hashCode() + assertEquals rsaPrivJwk2.hashCode(), rsaPrivJwk2.@context.hashCode() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSecureDigestAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSecureDigestAlgorithmTest.groovy new file mode 100644 index 00000000..4fd0b19d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractSecureDigestAlgorithmTest.groovy @@ -0,0 +1,96 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.SecureRequest +import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.VerifySecureDigestRequest +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.* + +import static org.junit.Assert.assertSame +import static org.junit.Assert.assertTrue + +class AbstractSecureDigestAlgorithmTest { + + @Test + void testSignAndVerifyWithExplicitProvider() { + Provider provider = Security.getProvider('BC') + def pair = Jwts.SIG.RS256.keyPairBuilder().build() + byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) + byte[] signature = Jwts.SIG.RS256.digest(new DefaultSecureRequest(data, provider, null, pair.getPrivate())) + assertTrue Jwts.SIG.RS256.verify(new DefaultVerifySecureDigestRequest(data, provider, null, pair.getPublic(), signature)) + } + + @Test + void testSignFailsWithAnExternalException() { + def pair = Jwts.SIG.RS256.keyPairBuilder().build() + def ise = new IllegalStateException('foo') + def alg = new TestAbstractSecureDigestAlgorithm() { + @Override + protected byte[] doDigest(SecureRequest request) throws Exception { + throw ise + } + } + try { + alg.digest(new DefaultSecureRequest('foo'.getBytes(StandardCharsets.UTF_8), null, null, pair.getPrivate())) + } catch (SignatureException e) { + assertTrue e.getMessage().startsWith('Unable to compute test signature with JCA algorithm \'test\' using key {') + assertTrue e.getMessage().endsWith('}: foo') + assertSame ise, e.getCause() + } + } + + @Test + void testVerifyFailsWithExternalException() { + def pair = Jwts.SIG.RS256.keyPairBuilder().build() + def ise = new IllegalStateException('foo') + def alg = new TestAbstractSecureDigestAlgorithm() { + @Override + protected boolean doVerify(VerifySecureDigestRequest request) throws Exception { + throw ise + } + } + def data = 'foo'.getBytes(StandardCharsets.UTF_8) + try { + byte[] signature = alg.digest(new DefaultSecureRequest(data, null, null, pair.getPrivate())) + alg.verify(new DefaultVerifySecureDigestRequest(data, null, null, pair.getPublic(), signature)) + } catch (SignatureException e) { + assertTrue e.getMessage().startsWith('Unable to verify test signature with JCA algorithm \'test\' using key {') + assertTrue e.getMessage().endsWith('}: foo') + assertSame ise, e.getCause() + } + } + + class TestAbstractSecureDigestAlgorithm extends AbstractSecureDigestAlgorithm { + + TestAbstractSecureDigestAlgorithm() { + super('test', 'test') + } + + @Override + protected void validateKey(Key key, boolean signing) { + } + + @Override + protected byte[] doDigest(SecureRequest request) throws Exception { + return new byte[1] + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy new file mode 100644 index 00000000..831dddbe --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesAlgorithmTest.groovy @@ -0,0 +1,135 @@ +/* + * 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.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.security.SecureRandom + +import static org.junit.Assert.* + +/** + * @since JJWT_RELEASE_VERSION + */ +class AesAlgorithmTest { + + @Test(expected = IllegalArgumentException) + void testConstructorWithoutRequiredKeyLength() { + new TestAesAlgorithm('foo', 'foo', 0) + } + + @Test + void testAssertKeyLength() { + + def alg = new TestAesAlgorithm('foo', 'foo', 192) + + SecretKey key = TestKeys.A128GCM //weaker than required + + Request request = new DefaultSecureRequest(new byte[1], null, null, key) + + try { + alg.assertKey(key) + fail() + } catch (SecurityException expected) { + } + } + + @Test + void testValidateLengthKeyExceptionPropagated() { + + def alg = new TestAesAlgorithm('foo', 'foo', 192) + def ex = new java.lang.SecurityException("HSM: not allowed") + def key = new SecretKeySpec(new byte[1], 'AES') { + @Override + byte[] getEncoded() { + throw ex + } + } + + try { + alg.validateLength(key, 192, true) + fail() + } catch (java.lang.SecurityException expected) { + assertSame ex, expected + } + } + + @Test + void testValidateLengthKeyExceptionNotPropagated() { + + def alg = new TestAesAlgorithm('foo', 'foo', 192) + def ex = new java.lang.SecurityException("HSM: not allowed") + def key = new SecretKeySpec(new byte[1], 'AES') { + @Override + byte[] getEncoded() { + throw ex + } + } + + //exception thrown, but we don't propagate: + assertNull alg.validateLength(key, 192, false) + } + + @Test + void testAssertBytesWithLengthMismatch() { + int reqdBitLen = 192 + def alg = new TestAesAlgorithm('foo', 'foo', reqdBitLen) + byte[] bytes = new byte[(reqdBitLen - 8) / Byte.SIZE] + try { + alg.assertBytes(bytes, 'test arrays', reqdBitLen) + fail() + } catch (IllegalArgumentException iae) { + String msg = "The 'foo' algorithm requires test arrays with a length of 192 bits (24 bytes). " + + "The provided key has a length of 184 bits (23 bytes)." + assertEquals msg, iae.getMessage() + } + } + + @Test + void testGetSecureRandomWhenRequestHasSpecifiedASecureRandom() { + + def alg = new TestAesAlgorithm('foo', 'foo', 128) + + def secureRandom = new SecureRandom() + + def req = new DefaultAeadRequest('data'.getBytes(), null, secureRandom, alg.keyBuilder().build(), 'aad'.getBytes()) + + def returnedSecureRandom = alg.ensureSecureRandom(req) + + assertSame(secureRandom, returnedSecureRandom) + } + + static class TestAesAlgorithm extends AesAlgorithm implements AeadAlgorithm { + + TestAesAlgorithm(String name, String transformationString, int requiredKeyLengthInBits) { + super(name, transformationString, requiredKeyLengthInBits) + } + + @Override + AeadResult encrypt(AeadRequest symmetricAeadRequest) { + return null + } + + @Override + Message decrypt(DecryptAeadRequest symmetricAeadDecryptionRequest) { + return null + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy new file mode 100644 index 00000000..27f89897 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -0,0 +1,177 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.impl.lang.CheckedSupplier +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.lang.Arrays +import io.jsonwebtoken.security.SecretKeyBuilder +import org.junit.Test + +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import java.security.Provider + +import static org.junit.Assert.* + +class AesGcmKeyAlgorithmTest { + + /** + * This tests asserts that our AeadAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm + * produce the exact same values. This should be the case when the transformation is identical, even though + * one uses Cipher.WRAP_MODE and the other uses a raw plaintext byte array. + */ + @Test + void testAesWrapProducesSameResultAsAesAeadEncryptionAlgorithm() { + + def alg = new GcmAesAeadAlgorithm(256) + + def iv = new byte[12] + Randoms.secureRandom().nextBytes(iv) + + def kek = alg.keyBuilder().build() + def cek = alg.keyBuilder().build() + + final String jcaName = "AES/GCM/NoPadding" + + // AES/GCM/NoPadding is only available on JDK 8 and later, so enable BC as a backup provider if + // necessary for <= JDK 7: + // TODO: remove when dropping Java 7 support: + Provider provider = Providers.findBouncyCastle(Conditions.notExists(new CheckedSupplier() { + @Override + SecretKeyFactory get() throws Exception { + return SecretKeyFactory.getInstance(jcaName) + } + })) + + JcaTemplate template = new JcaTemplate(jcaName, provider) + byte[] jcaResult = template.withCipher(new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + cipher.init(Cipher.WRAP_MODE, kek, new GCMParameterSpec(128, iv)) + return cipher.wrap(cek) + } + }) + + //separate tag from jca ciphertext: + int ciphertextLength = jcaResult.length - 16 //AES block size in bytes (128 bits) + byte[] ciphertext = new byte[ciphertextLength] + System.arraycopy(jcaResult, 0, ciphertext, 0, ciphertextLength) + + byte[] tag = new byte[16] + System.arraycopy(jcaResult, ciphertextLength, tag, 0, 16) + def resultA = new DefaultAeadResult(null, null, ciphertext, kek, null, tag, iv) + + def encRequest = new DefaultAeadRequest(cek.getEncoded(), null, null, kek, null, iv) + def encResult = Jwts.ENC.A256GCM.encrypt(encRequest) + + assertArrayEquals resultA.digest, encResult.digest + assertArrayEquals resultA.initializationVector, encResult.initializationVector + assertArrayEquals resultA.getPayload(), encResult.getPayload() + } + + static void assertAlgorithm(int keyLength) { + + def alg = new AesGcmKeyAlgorithm(keyLength) + assertEquals 'A' + keyLength + 'GCMKW', alg.getId() + + def template = new JcaTemplate('AES', null) + + def header = new DefaultJweHeader() + def kek = template.generateSecretKey(keyLength) + def cek = template.generateSecretKey(keyLength) + def enc = new GcmAesAeadAlgorithm(keyLength) { + @Override + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(cek) + } + } + + def ereq = new DefaultKeyRequest(kek, null, null, header, enc) + + def result = alg.getEncryptionKey(ereq) + + byte[] encryptedKeyBytes = result.getPayload() + assertFalse "encryptedKey must be populated", Arrays.length(encryptedKeyBytes) == 0 + + def dcek = alg.getDecryptionKey(new DefaultDecryptionKeyRequest(encryptedKeyBytes, null, null, header, enc, kek)) + + //Assert the decrypted key matches the original cek + assertEquals cek.algorithm, dcek.algorithm + assertArrayEquals cek.encoded, dcek.encoded + } + + @Test + void testResultSymmetry() { + assertAlgorithm(128) + assertAlgorithm(192) + assertAlgorithm(256) + } + + static void testDecryptionHeader(String headerName, Object value, String exmsg) { + int keyLength = 128 + def alg = new AesGcmKeyAlgorithm(keyLength) + def template = new JcaTemplate('AES', null) + def header = new DefaultJweHeader() + def kek = template.generateSecretKey(keyLength) + def cek = template.generateSecretKey(keyLength) + def enc = new GcmAesAeadAlgorithm(keyLength) { + @Override + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(cek) + } + } + def ereq = new DefaultKeyRequest(kek, null, null, header, enc) + def result = alg.getEncryptionKey(ereq) + + header.remove(headerName) + + header.put(headerName, value) + + byte[] encryptedKeyBytes = result.getPayload() + + try { + alg.getDecryptionKey(new DefaultDecryptionKeyRequest(encryptedKeyBytes, null, null, header, enc, kek)) + fail() + } catch (MalformedJwtException iae) { + assertEquals exmsg, iae.getMessage() + } + } + + static String missing(String id, String name) { + return "JWE header is missing required '$id' ($name) value." as String + } + + static String type(String name) { + return "JWE header '${name}' value must be a String. Actual type: java.lang.Integer" as String + } + + static String length(String name, int requiredBitLength) { + return "JWE header '${name}' decoded byte array must be ${Bytes.bitsMsg(requiredBitLength)} long. Actual length: ${Bytes.bitsMsg(16)}." + } + + @Test + void testMissingHeaders() { + testDecryptionHeader('iv', null, missing('iv', 'Initialization Vector')) + testDecryptionHeader('tag', null, missing('tag', 'Authentication Tag')) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy new file mode 100644 index 00000000..97231531 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConcatKDFTest.groovy @@ -0,0 +1,118 @@ +/* + * 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.security + +import io.jsonwebtoken.impl.lang.Bytes +import org.junit.Before +import org.junit.Test + +import javax.crypto.SecretKey +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +import static org.junit.Assert.* + +class ConcatKDFTest { + + ConcatKDF CONCAT_KDF = EcdhKeyAlgorithm.CONCAT_KDF + + private byte[] Z + + @Before + void setUp() { + Z = new byte[16] + Randoms.secureRandom().nextBytes(Z) + } + + @Test + void testNullOtherInfo() { + final int derivedKeyBitLength = 256 + final byte[] OtherInfo = null + + // exactly 1 Concat KDF iteration - derived key bit length of 256 is same as SHA-256 digest length: + def md = MessageDigest.getInstance("SHA-256") + md.update([0, 0, 0, 1] as byte[]) + md.update(Z) + md.update(Bytes.EMPTY) // null OtherInfo should equate to a Bytes.EMPTY argument here + byte[] digest = md.digest() + + SecretKey key = CONCAT_KDF.deriveKey(Z, derivedKeyBitLength, OtherInfo) + byte[] derived = key.getEncoded() + assertNotNull(key) + assertArrayEquals(digest, derived) + } + + @Test + void testEmptyOtherInfo() { + final int derivedKeyBitLength = 256 + final byte[] OtherInfo = Bytes.EMPTY + + // exactly 1 Concat KDF iteration - derived key bit length of 256 is same as SHA-256 digest length: + def md = MessageDigest.getInstance("SHA-256") + md.update([0, 0, 0, 1] as byte[]) + md.update(Z) + md.update(Bytes.EMPTY) // empty OtherInfo should equate to a Bytes.EMPTY argument here + byte[] digest = md.digest() + + SecretKey key = CONCAT_KDF.deriveKey(Z, derivedKeyBitLength, OtherInfo) + byte[] derived = key.getEncoded() + assertNotNull(key) + assertArrayEquals(digest, derived) + } + + @Test + void testPopulatedOtherInfo() { + final int derivedKeyBitLength = 256 + final byte[] OtherInfo = 'whatever'.getBytes(StandardCharsets.UTF_8) + + // exactly 1 Concat KDF iteration - derived key bit length of 256 is same as SHA-256 digest length: + def md = MessageDigest.getInstance("SHA-256") + md.update([0, 0, 0, 1] as byte[]) + md.update(Z) + md.update(OtherInfo) // ensure OtherInfo is included in the digest + byte[] digest = md.digest() + + SecretKey key = CONCAT_KDF.deriveKey(Z, derivedKeyBitLength, OtherInfo) + byte[] derived = key.getEncoded() + assertNotNull(key) + assertArrayEquals(digest, derived) + } + + @Test + void testNonPositiveBitLength() { + try { + CONCAT_KDF.deriveKey(Z, 0, null) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'derivedKeyBitLength must be a positive integer.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testDerivedKeyBitLengthBiggerThanJdkMax() { + byte[] Z = new byte[16] + long bitLength = Long.valueOf(Integer.MAX_VALUE) * 8L + 8L // one byte more than java byte arrays can handle + try { + CONCAT_KDF.deriveKey(Z, bitLength, null) + fail() + } catch (IllegalArgumentException expected) { + String msg = 'derivedKeyBitLength may not exceed 17179869176 bits (2147483647 bytes). ' + + 'Specified size: 17179869184 bits (2147483648 bytes).' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy new file mode 100644 index 00000000..ab432ff3 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ConstantKeyLocatorTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.DefaultJwsHeader +import io.jsonwebtoken.impl.DefaultUnprotectedHeader +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.* + +class ConstantKeyLocatorTest { + + @Test + void testSignatureVerificationKey() { + def key = new SecretKeySpec(new byte[1], 'AES') //dummy key for testing + assertSame key, new ConstantKeyLocator(key, null).locate(new DefaultJwsHeader()) + } + + @Test + void testSignatureVerificationKeyMissing() { + def locator = new ConstantKeyLocator(null, null) + try { + locator.locate(new DefaultJwsHeader()) + } catch (UnsupportedJwtException uje) { + String msg = 'Signed JWTs are not supported: the JwtParser has not been configured with a signature ' + + 'verification key or a KeyResolver. Consider configuring the JwtParserBuilder with one of these ' + + 'to ensure it can use the necessary key to verify JWS signatures.' + assertEquals msg, uje.getMessage() + } + } + + @Test + void testDecryptionKey() { + def key = new SecretKeySpec(new byte[1], 'AES') //dummy key for testing + assertSame key, new ConstantKeyLocator(null, key).locate(new DefaultJweHeader()) + } + + @Test + void testDecryptionKeyMissing() { + def locator = new ConstantKeyLocator(null, null) + try { + locator.locate(new DefaultJweHeader()) + } catch (UnsupportedJwtException uje) { + String msg = 'Encrypted JWTs are not supported: the JwtParser has not been configured with a decryption ' + + 'key or a KeyResolver. Consider configuring the JwtParserBuilder with one of these ' + + 'to ensure it can use the necessary key to decrypt JWEs.' + assertEquals msg, uje.getMessage() + } + } + + @Test + void testApply() { + def locator = new ConstantKeyLocator(null, null) + assertNull locator.apply(new DefaultUnprotectedHeader()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy new file mode 100644 index 00000000..97b0f7b3 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.security.Request +import org.junit.Test + +import java.security.Provider + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* + +class CryptoAlgorithmTest { + + @Test + void testEqualsSameInstance() { + def alg = new TestCryptoAlgorithm('test', 'test') + assertEquals alg, alg + } + + @Test + void testEqualsSameNameAndJcaName() { + def alg1 = new TestCryptoAlgorithm('test', 'test') + def alg2 = new TestCryptoAlgorithm('test', 'test') + assertEquals alg1, alg2 + } + + @Test + void testEqualsSameNameButDifferentJcaName() { + def alg1 = new TestCryptoAlgorithm('test', 'test1') + def alg2 = new TestCryptoAlgorithm('test', 'test2') + assertNotEquals alg1, alg2 + } + + @Test + void testEqualsOtherType() { + assertNotEquals new TestCryptoAlgorithm('test', 'test'), new Object() + } + + @Test + void testToString() { + assertEquals 'test', new TestCryptoAlgorithm('test', 'whatever').toString() + } + + @Test + void testHashCode() { + int hash = 7 + hash = 31 * hash + 'name'.hashCode() + hash = 31 * hash + 'jcaName'.hashCode() + assertEquals hash, new TestCryptoAlgorithm('name', 'jcaName').hashCode() + } + + @Test + void testEnsureSecureRandomWorksWithNullRequest() { + def alg = new TestCryptoAlgorithm('test', 'test') + def random = alg.ensureSecureRandom(null) + assertSame Randoms.secureRandom(), random + } + + @Test + void testRequestProviderPriorityOverDefaultProvider() { + + def alg = new TestCryptoAlgorithm('test', 'test') + + Provider defaultProvider = createMock(Provider) + Provider requestProvider = createMock(Provider) + Request request = createMock(Request) + alg.setProvider(defaultProvider) + + expect(request.getProvider()).andReturn(requestProvider) + + replay request, requestProvider, defaultProvider + + assertSame requestProvider, alg.getProvider(request) // assert we get back the request provider, not the default + + verify request, requestProvider, defaultProvider + } + + @Test + void testMissingRequestProviderUsesDefaultProvider() { + + def alg = new TestCryptoAlgorithm('test', 'test') + + Provider defaultProvider = createMock(Provider) + Request request = createMock(Request) + alg.setProvider(defaultProvider) + + expect(request.getProvider()).andReturn(null) + + replay request, defaultProvider + + assertSame defaultProvider, alg.getProvider(request) // assert we get back the default provider + + verify request, defaultProvider + } + + @Test + void testMissingRequestAndDefaultProviderReturnsNull() { + def alg = new TestCryptoAlgorithm('test', 'test') + Request request = createMock(Request) + expect(request.getProvider()).andReturn(null) + replay request + assertNull alg.getProvider(request) // null return value means use JCA internal default provider + verify request + } + + + class TestCryptoAlgorithm extends CryptoAlgorithm { + TestCryptoAlgorithm(String id, String jcaName) { + super(id, jcaName) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CurvesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CurvesTest.groovy new file mode 100644 index 00000000..77538b90 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CurvesTest.groovy @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 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.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +class CurvesTest { + + @Test + void testCtor() { + new Curves() // test coverage only + } + + @Test + void testFindById() { + Curves.VALUES.each { + it.equals(Curves.findById(it.getId())) + } + } + + @Test + void testFindByJcaName() { + Curves.VALUES.each { + it.equals(Curves.findByJcaName(it.getJcaName())) + } + } + + @Test + void testFindByEllipticCurve() { + Curves.EC_CURVES.each { + it.equals(Curves.findBy(it.toParameterSpec().getCurve())) + } + } + + @Test + void testKeyPairBuilders() { + Curves.VALUES.each { + def pair = it.keyPairBuilder().build() + if (it instanceof ECCurve) { + assertEquals ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, pair.getPublic().getAlgorithm() + assertEquals ECCurve.KEY_PAIR_GENERATOR_JCA_NAME, pair.getPrivate().getAlgorithm() + } else { // edwards curve + String jcaName = it.getJcaName() + String pubAlg = pair.getPublic().getAlgorithm() + String privAlg = pair.getPrivate().getAlgorithm() + + if (jcaName.startsWith('X')) { // X*** curves + //BC will retain exact alg, OpenJDK >= 11 will use 'XDH' instead, both are valid: + assertTrue(pubAlg.equals(jcaName) || pubAlg.equals('XDH')) + assertTrue(privAlg.equals(jcaName) || privAlg.equals('XDH')) + } else { // Ed*** curves + //BC will retain exact alg, OpenJDK >= 15 will use 'EdDSA' instead, both are valid: + assertTrue(pubAlg.equals(jcaName) || pubAlg.equals('EdDSA')) + assertTrue(privAlg.equals(jcaName) || privAlg.equals('EdDSA')) + } + } + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCurveTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCurveTest.groovy new file mode 100644 index 00000000..f72f877b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultCurveTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.security + +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.* + +class DefaultCurveTest { + + DefaultCurve curve + + @Before + void setUp() { + curve = new DefaultCurve('foo', 'bar') + } + + @Test + void testGetId() { + assertEquals 'foo', curve.getId() + } + + @Test + void testGetJcaName() { + assertEquals 'bar', curve.getJcaName() + } + + @Test + void testHashcode() { + assertEquals 'foo'.hashCode(), curve.hashCode() + } + + @Test + void testToString() { + assertEquals 'foo', curve.toString() + } + + @Test + void testEqualsIdentity() { + //noinspection ChangeToOperator + assertTrue curve.equals(curve) + } + + @Test + void testEqualsTypeMismatch() { + Object obj = new Integer(42) + //noinspection ChangeToOperator + assertFalse curve.equals(obj); + } + + @Test + void testEqualsId() { + def other = new DefaultCurve('foo', 'asdfasdf') + //noinspection ChangeToOperator + assertTrue curve.equals(other) + } + + @Test + void testNotEquals() { + def other = new DefaultCurve('abc', 'bar') + //noinspection ChangeToOperator + assertFalse curve.equals(other) + } + + @Test + void testKeyPairBuilder() { + def builder = curve.keyPairBuilder() + assertEquals 'bar', builder.jcaName //builder is an instanceof DefaultKeyPairBuilder + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultHashAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultHashAlgorithmTest.groovy new file mode 100644 index 00000000..d2d61919 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultHashAlgorithmTest.groovy @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.security.HashAlgorithm +import io.jsonwebtoken.security.StandardHashAlgorithms +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertTrue + +class DefaultHashAlgorithmTest { + + static final def algs = [DefaultHashAlgorithm.SHA1, StandardHashAlgorithms.get().SHA256] + + @Test + void testDigestAndVerify() { + byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8) + for (HashAlgorithm alg : algs) { + byte[] hash = alg.digest(new DefaultRequest(data, null, null)) + assertTrue alg.verify(new DefaultVerifyDigestRequest(data, null, null, hash)) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy new file mode 100644 index 00000000..3738c273 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkContextTest.groovy @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 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.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class DefaultJwkContextTest { + + @Test + void testGetName() { + def ctx = new DefaultJwkContext() + assertEquals 'JWK', ctx.getName() + } + + @Test + void testGetNameWhenSecretJwk() { + def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + ctx.put('kty', 'oct') + assertEquals 'Secret JWK', ctx.getName() + } + + @Test + void testGetNameWithGenericPublicKey() { + def ctx = new DefaultJwkContext() + ctx.setKey(TestKeys.ES256.pair.public) + assertEquals 'Public JWK', ctx.getName() + } + + @Test + void testGetNameWithGenericPrivateKey() { + def ctx = new DefaultJwkContext() + ctx.setKey(TestKeys.ES256.pair.private) + assertEquals 'Private JWK', ctx.getName() + } + + @Test + void testGetNameWithEdwardsPublicKey() { + def ctx = new DefaultJwkContext() + ctx.setKey(TestKeys.X448.pair.public) + ctx.setType(DefaultOctetPublicJwk.TYPE_VALUE) + assertEquals 'Octet Public JWK', ctx.getName() + } + + @Test + void testGetNameWithEdwardsPrivateKey() { + def ctx = new DefaultJwkContext() + ctx.setKey(TestKeys.X448.pair.private) + ctx.setType(DefaultOctetPublicJwk.TYPE_VALUE) + assertEquals 'Octet Private JWK', ctx.getName() + } + + @Test + void testGStringPrintsRedactedValues() { + // DO NOT REMOVE THIS METHOD: IT IS CRITICAL TO ENSURE GROOVY STRINGS DO NOT LEAK SECRET/PRIVATE KEY MATERIAL + def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + ctx.put('kty', 'oct') + ctx.put('k', 'test') + String s = '[kty:oct, k:]' + assertEquals "$s", "$ctx" + } + + @Test + void testGStringToStringPrintsRedactedValues() { + def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS) + ctx.put('kty', 'oct') + ctx.put('k', 'test') + String s = '{kty=oct, k=}' + assertEquals "$s", "${ctx.toString()}" + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy new file mode 100644 index 00000000..5608ed15 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.security.Provider + +import static org.easymock.EasyMock.createMock +import static org.junit.Assert.* + +class DefaultJwkParserBuilderTest { + + @Test + void testDefault() { + def builder = Jwks.parser() as DefaultJwkParserBuilder + assertNotNull builder + assertNull builder.provider + assertNull builder.deserializer + def parser = builder.build() as DefaultJwkParser + assertNull parser.provider + assertNotNull parser.deserializer // Services.loadFirst should have picked one up + } + + @Test + void testProvider() { + def provider = createMock(Provider) + def parser = Jwks.parser().setProvider(provider).build() as DefaultJwkParser + assertSame provider, parser.provider + } + + @Test + void testDeserializer() { + def deserializer = createMock(Deserializer) + def parser = Jwks.parser().deserializeJsonWith(deserializer).build() as DefaultJwkParser + assertSame deserializer, parser.deserializer + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy new file mode 100644 index 00000000..43355c5d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserTest.groovy @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.DeserializationException +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.Key + +import static org.junit.Assert.* + +class DefaultJwkParserTest { + + @Test + void testKeys() { + + Set keys = new LinkedHashSet<>() + TestKeys.HS.each { keys.add(it) } + TestKeys.RSA.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + TestKeys.EC.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + + def serializer = Services.loadFirst(Serializer) + for (Key key : keys) { + //noinspection GroovyAssignabilityCheck + def jwk = Jwks.builder().forKey(key).build() + def data = serializer.serialize(jwk) + String json = new String(data, StandardCharsets.UTF_8) + def parsed = Jwks.parser().build().parse(json) + assertEquals jwk, parsed + } + } + + @Test + void testKeysWithProvider() { + + Set keys = new LinkedHashSet<>() + TestKeys.HS.each { keys.add(it) } + TestKeys.RSA.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + TestKeys.EC.each { + keys.add(it.pair.public) + keys.add(it.pair.private) + } + + def serializer = Services.loadFirst(Serializer) + def provider = Providers.findBouncyCastle(Conditions.TRUE) + + for (Key key : keys) { + //noinspection GroovyAssignabilityCheck + def jwk = Jwks.builder().forKey(key).build() + def data = serializer.serialize(jwk) + String json = new String(data, StandardCharsets.UTF_8) + def parsed = Jwks.parser().setProvider(provider).build().parse(json) + assertEquals jwk, parsed + assertSame provider, parsed.@context.@provider + } + } + + @Test + void testDeserializationFailure() { + def parser = new DefaultJwkParser(null, Services.loadFirst(Deserializer)) { + @Override + protected Map deserialize(String json) { + throw new DeserializationException("test") + } + } + try { + parser.parse('foo') + fail() + } catch (MalformedKeyException expected) { + String msg = "Unable to deserialize JSON string argument: test" + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkTest.groovy new file mode 100644 index 00000000..3570c0d6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkTest.groovy @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2018 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.security + +class DefaultJwkTest { + + /* + + private static final SecretKey TEST_KEY = StandardSecureDigestAlgorithms.HS512.generateKey(); + + class TestJwk extends AbstractJwk { + TestJwk(String type = "test", String use = null, Set operations = null, String algorithm = null, String id = null, URI x509url = null, List certChain = null, byte[] x509Sha1Thumbprint = null, byte[] x509Sha256Thumbprint = null, Key key = TEST_KEY) { + super(type, use, operations, algorithm, id, x509url, certChain, x509Sha1Thumbprint, x509Sha256Thumbprint, TEST_KEY, null) + } + } + + @Test + void testType() { + assertEquals "test", new TestJwk().getType() + } + + @Test + void testUse() { + def jwk = new TestJwk() + + assertEquals 'use', AbstractJwk.USE + assertNull jwk.get(AbstractJwk.USE) + assertNull jwk.getUse() + + jwk = new TestJwk(use: ' ') //empty should remove + assertNull jwk.get(AbstractJwk.USE) + assertNull jwk.getUse() + + String val = UUID.randomUUID().toString() + jwk = new TestJwk(use: val) + assertEquals val, jwk.get(AbstractJwk.USE) + assertEquals val, jwk.getUse() + } + + @Test + void testOperations() { + + def jwk = new TestJwk() + + assertEquals 'key_ops', AbstractJwk.OPERATIONS + + jwk.setOperations(null) + assertNull jwk.get(AbstractJwk.OPERATIONS) + assertNull jwk.getOperations() + + jwk.setOperations([] as Set) //empty should remove + assertNull jwk.get(AbstractJwk.OPERATIONS) + assertNull jwk.getOperations() + + def set = ['a', 'b'] as Set + + jwk.setOperations(set) + assertEquals set, jwk.get(AbstractJwk.OPERATIONS) + assertEquals set, jwk.getOperations() + } + + @Test + void testAlgorithm() { + def jwk = new TestJwk() + + assertEquals 'alg', AbstractJwk.ALGORITHM + + jwk.setAlgorithm(null) + assertNull jwk.get(AbstractJwk.ALGORITHM) + assertNull jwk.getAlgorithm() + + jwk.setAlgorithm(' ') //empty should remove + assertNull jwk.get(AbstractJwk.ALGORITHM) + assertNull jwk.getAlgorithm() + + String val = UUID.randomUUID().toString() + jwk.setAlgorithm(val) + assertEquals val, jwk.get(AbstractJwk.ALGORITHM) + assertEquals val, jwk.getAlgorithm() + } + + @Test + void testId() { + def jwk = new TestJwk() + + assertEquals 'kid', AbstractJwk.ID + + jwk.setId(null) + assertNull jwk.get(AbstractJwk.ID) + assertNull jwk.getId() + + jwk.setId(' ') //empty should remove + assertNull jwk.get(AbstractJwk.ID) + assertNull jwk.getId() + + String val = UUID.randomUUID().toString() + jwk.setId(val) + assertEquals val, jwk.get(AbstractJwk.ID) + assertEquals val, jwk.getId() + } + + @Test + void testX509Sha1Thumbprint() { + def jwk = new TestJwk() + + assertEquals 'x5t', AbstractJwk.X509_SHA1_THUMBPRINT + + jwk.setX509CertificateSha1Thumbprint(null) + assertNull jwk.get(AbstractJwk.X509_SHA1_THUMBPRINT) + assertNull jwk.getX509CertificateSha1Thumbprint() + + jwk.setX509CertificateSha1Thumbprint(' ') //empty should remove + assertNull jwk.get(AbstractJwk.X509_SHA1_THUMBPRINT) + assertNull jwk.getX509CertificateSha1Thumbprint() + + String val = UUID.randomUUID().toString() + jwk.setX509CertificateSha1Thumbprint(val) + assertEquals val, jwk.get(AbstractJwk.X509_SHA1_THUMBPRINT) + assertEquals val, jwk.getX509CertificateSha1Thumbprint() + } + + @Test + void testX509Sha256Thumbprint() { + def jwk = new TestJwk() + + assertEquals 'x5t#S256', AbstractJwk.X509_SHA256_THUMBPRINT + + jwk.setX509CertificateSha1Thumbprint(null) + assertNull jwk.get(AbstractJwk.X509_SHA256_THUMBPRINT) + assertNull jwk.getX509CertificateSha256Thumbprint() + + jwk.setX509CertificateSha256Thumbprint(' ') //empty should remove + assertNull jwk.get(AbstractJwk.X509_SHA256_THUMBPRINT) + assertNull jwk.getX509CertificateSha256Thumbprint() + + String val = UUID.randomUUID().toString() + jwk.setX509CertificateSha256Thumbprint(val) + assertEquals val, jwk.get(AbstractJwk.X509_SHA256_THUMBPRINT) + assertEquals val, jwk.getX509CertificateSha256Thumbprint() + } + + @Test + void testX509Url() { + + def jwk = new TestJwk() + + assertEquals 'x5u', AbstractJwk.X509_URL + + jwk.setX509Url(null) + assertNull jwk.get(AbstractJwk.X509_URL) + assertNull jwk.getX509Url() + + String suri = 'https://whatever.com/cert' + def uri = new URI(suri) + + jwk.put(AbstractJwk.X509_URL, uri) + assertEquals uri, jwk.get(AbstractJwk.X509_URL) + assertEquals uri, jwk.getX509Url() + + jwk.put(AbstractJwk.X509_URL, suri) + assertEquals suri, jwk.get(AbstractJwk.X509_URL) //string here + assertEquals uri, jwk.getX509Url() //conversion here + assertEquals uri, jwk.get(AbstractJwk.X509_URL) //ensure replaced with URI instance + + jwk.remove(AbstractJwk.X509_URL) //clear for next test + + jwk.setX509Url(uri) + assertEquals uri, jwk.get(AbstractJwk.X509_URL) + assertEquals uri, jwk.getX509Url() + } + + @Test + void testGetX509UrlWithInvalidUri() { + def jwk = new TestJwk() + def uri = '|not-a-uri|' + jwk.put(AbstractJwk.X509_URL, uri) + try { + jwk.getX509Url() + fail() + } catch (MalformedKeyException e) { + assertEquals 'test JWK x5u value cannot be converted to a URI instance: ' + uri, e.getMessage() + assertTrue e.getCause() instanceof URISyntaxException + } + } + + @Test + void testGetListWithNullValue() { + assertNull new TestJwk().getList("foo") + } + + @Test + void testGetX509CertChainWithSet() { + def jwk = new TestJwk() + jwk.put('x5c', new LinkedHashSet<>(['a', null, 'b'])) + def chain = jwk.getX509CertificateChain() + assertTrue chain instanceof List + assertEquals 3, chain.size() + assertEquals 'a', chain[0] + assertNull chain[1] + assertEquals 'b', chain[2] + } + + @Test + void testGetX509CertChainWithArray() { + def jwk = new TestJwk() + jwk.put('x5c', ['a', null, 'b'] as String[]) + def chain = jwk.getX509CertificateChain() + assertTrue chain instanceof List + assertEquals 3, chain.size() + assertEquals 'a', chain[0] + assertNull chain[1] + assertEquals 'b', chain[2] + } + + @Test + void testSetX509CertChain() { + + def jwk = new TestJwk() + + assertEquals 'x5c', AbstractJwk.X509_CERT_CHAIN + + jwk.setX509CertificateChain(null) + assertNull jwk.get(AbstractJwk.X509_CERT_CHAIN) + assertNull jwk.getX509CertificateChain() + + jwk.setX509CertificateChain([]) + assertNull jwk.get(AbstractJwk.X509_CERT_CHAIN) + assertNull jwk.getX509CertificateChain() + + String val = UUID.randomUUID().toString() + def chain = [val] + jwk.setX509CertificateChain(chain) + assertEquals chain, jwk.get(AbstractJwk.X509_CERT_CHAIN) + assertEquals chain, jwk.getX509CertificateChain() + } + + */ +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkThumbprintTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkThumbprintTest.groovy new file mode 100644 index 00000000..369ae116 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkThumbprintTest.groovy @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.HashAlgorithm +import io.jsonwebtoken.security.StandardHashAlgorithms +import org.junit.Before +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +import static org.junit.Assert.* + +class DefaultJwkThumbprintTest { + + private static String content = "Hello World" + private static HashAlgorithm alg = StandardHashAlgorithms.get().SHA256 + private static byte[] digest = alg.digest(new DefaultRequest(content.getBytes(StandardCharsets.UTF_8), null, null)) + private static String expectedToString = Encoders.BASE64URL.encode(digest) + private static String expectedUriString = DefaultJwkThumbprint.URI_PREFIX + alg.getId() + ":" + expectedToString + private static URI expectedUri = URI.create(expectedUriString) + + private DefaultJwkThumbprint thumbprint + + @Before + void setUp() { + this.thumbprint = new DefaultJwkThumbprint(digest, alg) + } + + @Test + void testGetHashAlgorithm() { + assertSame alg, thumbprint.getHashAlgorithm() + } + + @Test + void testToByteArray() { + assertTrue MessageDigest.isEqual(digest, thumbprint.toByteArray()) + } + + @Test + void testToURI() { + assertEquals expectedUri, thumbprint.toURI() + } + + @Test + void testHashCode() { + assertEquals io.jsonwebtoken.lang.Objects.nullSafeHashCode(digest, alg), thumbprint.hashCode() + } + + @Test + void testIdentityEquals() { + assertEquals thumbprint, thumbprint + } + + @Test + void testPropertyEquals() { + assertEquals thumbprint, new DefaultJwkThumbprint(digest, alg) + } + + @Test + void testNotEquals() { + // invalid data type: + assertNotEquals new DefaultJwkThumbprint(digest, alg), new Object() + + // same digest, different alg: + assertFalse thumbprint == new DefaultJwkThumbprint(digest, DefaultHashAlgorithm.SHA1) + + // same alg, different digest: + byte[] digest2 = alg.digest(new DefaultRequest("Hello World!".getBytes(StandardCharsets.UTF_8), null, null)) + assertFalse thumbprint == new DefaultJwkThumbprint(digest2, DefaultHashAlgorithm.SHA1) + } + + @Test + void testToString() { + assertEquals expectedToString, thumbprint.toString() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyPairBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyPairBuilderTest.groovy new file mode 100644 index 00000000..b427a98a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyPairBuilderTest.groovy @@ -0,0 +1,38 @@ +/* + * 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.security + +import org.junit.Test + +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.assertNotNull +import static org.junit.Assert.assertTrue + +class DefaultKeyPairBuilderTest { + + @Test + // simple test - just a JCA name, no extra config + void testSimpleBuild() { + def pair = new DefaultKeyPairBuilder("EC").build() + assertNotNull pair + assertNotNull pair.getPrivate() + assertNotNull pair.getPublic() + assertTrue pair.getPrivate() instanceof ECPrivateKey + assertTrue pair.getPublic() instanceof ECPublicKey + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy new file mode 100644 index 00000000..6cdf2549 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultKeyUseStrategyTest.groovy @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 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.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNull + +class DefaultKeyUseStrategyTest { + + final KeyUseStrategy strat = DefaultKeyUseStrategy.INSTANCE + + private static KeyUsage usage(int trueIndex) { + boolean[] usage = new boolean[9] + usage[trueIndex] = true + return new KeyUsage(new TestX509Certificate(keyUsage: usage)) + } + + @Test + void testKeyEncipherment() { + assertEquals 'enc', strat.toJwkValue(usage(2)) + } + + @Test + void testDataEncipherment() { + assertEquals 'enc', strat.toJwkValue(usage(3)) + } + + @Test + void testKeyAgreement() { + assertEquals 'enc', strat.toJwkValue(usage(4)) + } + + @Test + void testDigitalSignature() { + assertEquals 'sig', strat.toJwkValue(usage(0)) + } + + @Test + void testNonRepudiation() { + assertEquals 'sig', strat.toJwkValue(usage(1)) + } + + @Test + void testKeyCertSign() { + assertEquals 'sig', strat.toJwkValue(usage(5)) + } + + @Test + void testCRLSign() { + assertEquals 'sig', strat.toJwkValue(usage(6)) + } + + @Test + void testEncipherOnly() { + assertNull strat.toJwkValue(usage(7)) + } + + @Test + void testDecipherOnly() { + assertNull strat.toJwkValue(usage(8)) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy new file mode 100644 index 00000000..0944b57d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.Key + +import static org.junit.Assert.assertEquals + +class DefaultMacAlgorithmTest { + + static final byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8) + static final char[] passwordChars = "correct horse battery staple".toCharArray() + + static SecureRequest request(T key) { + return new DefaultSecureRequest(payload, null, null, key) + } + + static DefaultMacAlgorithm newAlg() { + return new DefaultMacAlgorithm('HS256', 'HmacSHA256', 256) + } + + /** + * Asserts a default Password instance can't be used (poor length/entropy) + */ + @Test + void testWithPasswordSpec() { + def password = Keys.forPassword(passwordChars) + try { + newAlg().digest(request(password)) + } catch (InvalidKeyException expected) { + String msg = 'The signing key\'s algorithm \'NONE\' does not equal a valid HmacSHA* algorithm name or PKCS12 OID and cannot be used with HS256.' + assertEquals msg, expected.getMessage() + } + } + + /** + * Asserts a Password instance that fakes a valid HmacSHA* JDK algorithm name can't be used + */ + @Test + void testCustomPasswordWithValidAlgorithm() { + def password = new PasswordSpec("correct horse battery staple".toCharArray()) { + @Override + String getAlgorithm() { + return 'HmacSHA256' + } + } + try { + newAlg().digest(request(password)) + } catch (SignatureException expected) { + String msg = "Unable to compute HS256 signature with JCA algorithm 'HmacSHA256' using key {${KeysBridge.toString(password)}}: ${expected.getCause().getMessage()}" as String + assertEquals msg, expected.getMessage() + } + } + + /** + * Asserts a Password instance that fakes a valid HmacSHA* JDK algorithm name, and even has encoded bytes can't be used + */ + @Test + void testWithCustomPasswordGetEncodedThrowsException() { + Password password = new PasswordSpec("correct horse".toCharArray()) { + @Override + String getAlgorithm() { + return 'HmacSHA256' + } + + @Override + byte[] getEncoded() { + throw new UnsupportedOperationException("Invalid") + } + } + + try { + newAlg().digest(request(password)) + } catch (SignatureException expected) { + String msg = "Unable to compute HS256 signature with JCA algorithm 'HmacSHA256' using key {${KeysBridge.toString(password)}}: ${expected.getCause().getMessage()}" as String + assertEquals msg, expected.getMessage() + } + } + + @Test(expected = SecurityException) + void testKeyGeneratorNoSuchAlgorithm() { + DefaultMacAlgorithm alg = new DefaultMacAlgorithm('HS256', 'foo', 256) + alg.keyBuilder().build() + } + + @Test + void testKeyGeneratorKeyLength() { + DefaultMacAlgorithm alg = new DefaultMacAlgorithm('HS256', 'HmacSHA256', 256) + assertEquals 256, alg.keyBuilder().build().getEncoded().length * Byte.SIZE + + alg = new DefaultMacAlgorithm('A128CBC-HS256', 'HmacSHA256', 128) + assertEquals 128, alg.keyBuilder().build().getEncoded().length * Byte.SIZE + } + + @Test(expected = IllegalArgumentException) + void testValidateNullKey() { + newAlg().validateKey(null, true) + } + + @Test(expected = InvalidKeyException) + void testValidateKeyNoAlgorithm() { + newAlg().validateKey(new SecretKeySpec(new byte[1], ' '), true) + } + + @Test(expected = InvalidKeyException) + void testValidateKeyInvalidJcaAlgorithm() { + newAlg().validateKey(new SecretKeySpec(new byte[1], 'foo'), true) + } + + @Test + void testValidateKeyEncodedNotAvailable() { + def key = new SecretKeySpec(new byte[1], 'HmacSHA256') { + @Override + byte[] getEncoded() { + throw new UnsupportedOperationException("HSM: not allowed") + } + } + newAlg().validateKey(key, true) + } + + @Test + void testValidateKeyStandardAlgorithmWeakKey() { + byte[] bytes = new byte[24] + Randoms.secureRandom().nextBytes(bytes) + try { + newAlg().validateKey(new SecretKeySpec(bytes, 'HmacSHA256'), true) + } catch (WeakKeyException expected) { + String msg = 'The signing key\'s size is 192 bits which is not secure enough for the HS256 algorithm. ' + + 'The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HS256 MUST have a ' + + 'size >= 256 bits (the key size must be greater than or equal to the hash output size). ' + + 'Consider using the StandardSecureDigestAlgorithms.HS256.keyBuilder() method to create a key guaranteed ' + + 'to be secure enough for HS256. See https://tools.ietf.org/html/rfc7518#section-3.2 for more ' + + 'information.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testValidateKeyCustomAlgorithmWeakKey() { + byte[] bytes = new byte[24] + Randoms.secureRandom().nextBytes(bytes) + DefaultMacAlgorithm alg = new DefaultMacAlgorithm('foo', 'foo', 256) + try { + alg.validateKey(new SecretKeySpec(bytes, 'HmacSHA256'), true) + } catch (WeakKeyException expected) { + assertEquals 'The signing key\'s size is 192 bits which is not secure enough for the foo algorithm. The foo algorithm requires keys to have a size >= 256 bits.', expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMessageTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMessageTest.groovy new file mode 100644 index 00000000..a9786841 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMessageTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 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.security + +import org.junit.Test + +class DefaultMessageTest { + + @Test(expected = IllegalArgumentException) + void testNullData() { + new DefaultMessage(null) + } + + @Test(expected = IllegalArgumentException) + void testEmptyByteArrayData() { + new DefaultMessage(new byte[0]) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy new file mode 100644 index 00000000..103671e0 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.WeakKeyException +import org.junit.Test + +import javax.crypto.SecretKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.easymock.EasyMock.* +import static org.junit.Assert.assertEquals + +class DefaultRsaKeyAlgorithmTest { + + static final algs = [Jwts.KEY.RSA1_5, Jwts.KEY.RSA_OAEP, Jwts.KEY.RSA_OAEP_256] as List + + @Test + void testValidateNonRSAKey() { + SecretKey key = Jwts.KEY.A128KW.keyBuilder().build() + for (DefaultRsaKeyAlgorithm alg : algs) { + // if RSAKey interface isn't exposed (e.g. PKCS11 or HSM), don't error: + alg.validate(key, true) + alg.validate(key, false) + } + } + + @Test + void testWeakEncryptionKey() { + for (DefaultRsaKeyAlgorithm alg : algs) { + RSAPublicKey key = createMock(RSAPublicKey) + expect(key.getModulus()).andReturn(BigInteger.ONE) + replay(key) + try { + alg.validate(key, true) + } catch (WeakKeyException e) { + String id = alg.getId() + String section = id.equals("RSA1_5") ? "4.2" : "4.3" + String msg = "The RSA encryption key's size (modulus) is 1 bits which is not secure enough for " + + "the $id algorithm. The JWT JWA Specification (RFC 7518, Section $section) states that " + + "RSA keys MUST have a size >= 2048 bits. " + + "See https://www.rfc-editor.org/rfc/rfc7518.html#section-$section for more information." + assertEquals(msg, e.getMessage()) + } + verify(key) + } + } + + @Test + void testWeakDecryptionKey() { + for (DefaultRsaKeyAlgorithm alg : algs) { + RSAPrivateKey key = createMock(RSAPrivateKey) + expect(key.getModulus()).andReturn(BigInteger.ONE) + replay(key) + try { + alg.validate(key, false) + } catch (WeakKeyException e) { + String id = alg.getId() + String section = id.equals("RSA1_5") ? "4.2" : "4.3" + String msg = "The RSA decryption key's size (modulus) is 1 bits which is not secure enough for " + + "the $id algorithm. The JWT JWA Specification (RFC 7518, Section $section) states that " + + "RSA keys MUST have a size >= 2048 bits. " + + "See https://www.rfc-editor.org/rfc/rfc7518.html#section-$section for more information." + assertEquals(msg, e.getMessage()) + } + verify(key) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy similarity index 57% rename from impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy index a09c198a..5077a9a8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilderTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 jsonwebtoken.io + * 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. @@ -13,25 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto +package io.jsonwebtoken.impl.security -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys import org.junit.Test import static org.junit.Assert.assertEquals import static org.junit.Assert.fail -class DefaultSignatureValidatorFactoryTest { +class DefaultSecretKeyBuilderTest { @Test - void testNoneAlgorithm() { + void testInvalidBitLength() { try { - new DefaultSignatureValidatorFactory().createSignatureValidator( - SignatureAlgorithm.NONE, Keys.secretKeyFor(SignatureAlgorithm.HS256)) + //noinspection GroovyResultOfObjectAllocationIgnored + new DefaultSecretKeyBuilder("AES", 127) fail() - } catch (IllegalArgumentException iae) { - assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." + } catch (IllegalArgumentException expected) { + String msg = "bitLength must be an even multiple of 8" + assertEquals msg, expected.getMessage() } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy new file mode 100644 index 00000000..334bfcb7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.lang.Arrays +import io.jsonwebtoken.security.DecryptionKeyRequest +import org.junit.Test + +import javax.crypto.spec.SecretKeySpec +import java.security.Key + +import static org.easymock.EasyMock.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class DirectKeyAlgorithmTest { + + @Test + void testId() { + assertEquals "dir", new DirectKeyAlgorithm().getId() + } + + @Test + void testGetEncryptionKey() { + def alg = new DirectKeyAlgorithm() + def key = new SecretKeySpec(new byte[1], "AES") + def request = new DefaultKeyRequest(key, null, null, new DefaultJweHeader(), Jwts.ENC.A128GCM) + def result = alg.getEncryptionKey(request) + assertSame key, result.getKey() + assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key + } + + @Test(expected = IllegalArgumentException) + void testGetEncryptionKeyWithNullRequest() { + new DirectKeyAlgorithm().getEncryptionKey(null) + } + + @Test(expected = IllegalArgumentException) + void testGetEncryptionKeyWithNullRequestKey() { + def key = new SecretKeySpec(new byte[1], "AES") + def request = new DefaultKeyRequest(key, null, null, new DefaultJweHeader(), Jwts.ENC.A128GCM) { + @Override + Key getPayload() { + return null + } + } + new DirectKeyAlgorithm().getEncryptionKey(request) + } + + @Test + void testGetDecryptionKey() { + def alg = new DirectKeyAlgorithm() + DecryptionKeyRequest req = createMock(DecryptionKeyRequest) + def key = Jwts.ENC.A128GCM.keyBuilder().build() + expect(req.getKey()).andReturn(key) + replay(req) + def result = alg.getDecryptionKey(req) + verify(req) + assertSame key, result + } + + @Test(expected = IllegalArgumentException) + void testGetDecryptionKeyWithNullRequest() { + new DirectKeyAlgorithm().getDecryptionKey(null) + } + + @Test(expected = IllegalArgumentException) + void testGetDecryptionKeyWithNullRequestKey() { + DecryptionKeyRequest req = createMock(DecryptionKeyRequest) + expect(req.getKey()).andReturn(null) + replay(req) + new DirectKeyAlgorithm().getDecryptionKey(req) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy new file mode 100644 index 00000000..612b75fa --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.Key +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey + +import static org.junit.Assert.* + +class DispatchingJwkFactoryTest { + + @Test(expected = IllegalArgumentException) + void testNullJwk() { + new DispatchingJwkFactory().createJwk(null) + } + + @Test(expected = InvalidKeyException) + void testEmptyJwk() { + new DispatchingJwkFactory().createJwk(new DefaultJwkContext()) + } + + @Test(expected = UnsupportedKeyException) + void testUnknownKtyValue() { + def ctx = new DefaultJwkContext() + ctx.put('kty', 'foo') + new DispatchingJwkFactory().createJwk(ctx) + } + + @Test + void testNewContextNoFamily() { + def ctx = new DefaultJwkContext() + def key = new TestKey(algorithm: 'foo') + try { + DispatchingJwkFactory.DEFAULT_INSTANCE.newContext(ctx, key) + fail() + } catch (UnsupportedKeyException uke) { + String msg = 'Unable to create JWK for unrecognized key of type io.jsonwebtoken.impl.security.TestKey: ' + + 'there is no known JWK Factory capable of creating JWKs for this key type.' + assertEquals msg, uke.getMessage() + } + } + + @Test + void testUnknownKeyType() { + def key = new Key() { + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + } + def ctx = new DefaultJwkContext().setKey(key) + try { + new DispatchingJwkFactory().createJwk(ctx) + fail() + } catch (UnsupportedKeyException uke) { + String msg = 'Unable to create JWK for unrecognized key of type io.jsonwebtoken.impl.security.DispatchingJwkFactoryTest$1: there is no known JWK Factory capable of creating JWKs for this key type.' + assertEquals msg, uke.getMessage() + } + } + + @Test + void testEcKeyPairToKey() { + + Map m = [ + 'kty': 'EC', + 'crv': 'P-256', + "x" : "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y" : "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", + "d" : "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" + ] + + def ctx = new DefaultJwkContext() + ctx.putAll(m) + + DispatchingJwkFactory factory = new DispatchingJwkFactory() + ctx = factory.newContext(ctx, null) + + def jwk = factory.createJwk(ctx) as EcPrivateJwk + assertTrue jwk instanceof EcPrivateJwk + def key = jwk.toKey() + assertTrue key instanceof ECPrivateKey + String x = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineX) + String y = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineY) + String d = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, key.s) + assertEquals jwk.d.get(), d + + //remove the 'd' mapping to represent only a public key: + m.remove(DefaultEcPrivateJwk.D.getId()) + ctx = new DefaultJwkContext() + ctx.putAll(m) + ctx = factory.newContext(ctx, null) + + jwk = factory.createJwk(ctx) as EcPublicJwk + assertTrue jwk instanceof EcPublicJwk + key = jwk.toKey() as ECPublicKey + assertTrue key instanceof ECPublicKey + assertEquals jwk.x, x + assertEquals jwk.y, y + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy new file mode 100644 index 00000000..3040acba --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ECCurveTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.security + +import org.junit.Test + +import java.security.interfaces.ECPublicKey +import java.security.spec.ECPoint + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +class ECCurveTest { + + @Test + void testContainsTrue() { + ECCurve curve = (ECCurve) Curves.P_256 + def pair = curve.keyPairBuilder().build() + ECPublicKey ecPub = (ECPublicKey) pair.getPublic() + assertTrue(curve.contains(ecPub.getW())) + } + + @Test + void testContainsFalse() { + assertFalse(((ECCurve) Curves.P_256).contains(new ECPoint(BigInteger.ONE, BigInteger.ONE))) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy new file mode 100644 index 00000000..992149b0 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPrivateJwkFactoryTest.groovy @@ -0,0 +1,39 @@ +/* + * 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.security + +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class EcPrivateJwkFactoryTest { + + @Test + void testDMissing() { + def values = ['kty': 'EC', 'crv': 'P-256', 'x': BigInteger.ONE, 'y': BigInteger.ONE] + try { + def ctx = new DefaultJwkContext(DefaultEcPrivateJwk.FIELDS) + ctx.putAll(values) + new EcPrivateJwkFactory().createJwkFromValues(ctx) + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'd' (ECC Private Key) value." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy new file mode 100644 index 00000000..04f1ed6a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcPublicJwkFactoryTest.groovy @@ -0,0 +1,75 @@ +/* + * 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.security + +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class EcPublicJwkFactoryTest { + + @Test + void testCurveMissing() { + try { + Jwks.builder().putAll(['kty': 'EC']).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'crv' (Curve) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testXMissing() { + try { + Jwks.builder().putAll(['kty': 'EC', 'crv': 'P-256']).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'x' (X Coordinate) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testYMissing() { + try { + String encoded = DefaultEcPublicJwk.X.applyTo(BigInteger.ONE) + Jwks.builder().putAll(['kty': 'EC', 'crv': 'P-256', 'x': encoded]).build() + fail() + } catch (MalformedKeyException expected) { + String msg = "EC JWK is missing required 'y' (Y Coordinate) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testPointNotOnCurve() { + try { + String encoded = DefaultEcPublicJwk.X.applyTo(BigInteger.ONE) + Jwks.builder().putAll(['kty': 'EC', 'crv': 'P-256', 'x': encoded, 'y': encoded]).build() + fail() + } catch (InvalidKeyException expected) { + String msg = "EC JWK x,y coordinates do not exist on elliptic curve 'P-256'. " + + "This could be due simply to an incorrectly-created JWK or possibly an attempted " + + "Invalid Curve Attack (see https://safecurves.cr.yp.to/twist.html for more information)." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy new file mode 100644 index 00000000..16c057bb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2018 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. + */ +//file:noinspection SpellCheckingInspection +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.JwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.Signature +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.EllipticCurve +import java.security.spec.X509EncodedKeySpec + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* + +class EcSignatureAlgorithmTest { + + static Collection algs() { + return Jwts.SIG.values().findAll({ it instanceof EcSignatureAlgorithm }) as Collection + } + + @Test + void testConstructorWithWeakKeyLength() { + try { + new EcSignatureAlgorithm(128) + } catch (IllegalArgumentException iae) { + String msg = 'orderBitLength must equal 256, 384, or 521.' + assertEquals msg, iae.getMessage() + } + } + + @Test + void testValidateKeyWithoutEcKey() { + def key = createMock(PublicKey) + replay key + algs().each { + it.validateKey(key, false) + //no exception - can't check for ECKey fields (e.g. PKCS11 or HSM key) + } + verify key + } + + @Test + void testIsValidRAndSWithoutEcKey() { + def key = createMock(PublicKey) + replay key + algs().each { + it.isValidRAndS(key, Bytes.EMPTY) + //no exception - can't check for ECKey fields (e.g. PKCS11 or HSM key) + } + verify key + } + + @Test + void testSignWithPublicKey() { + ECPublicKey key = TestKeys.ES256.pair.public as ECPublicKey + def request = new DefaultSecureRequest(new byte[1], null, null, key) + def alg = Jwts.SIG.ES256 + try { + alg.digest(request) + } catch (InvalidKeyException e) { + String msg = "${alg.getId()} signing keys must be PrivateKeys (implement ${PrivateKey.class.getName()}). " + + "Provided key type: ${key.getClass().getName()}." + assertEquals msg, e.getMessage() + } + } + + @Test + void testSignWithWeakKey() { + algs().each { + BigInteger order = BigInteger.ONE + ECParameterSpec spec = new ECParameterSpec(new EllipticCurve(new TestECField(), BigInteger.ONE, BigInteger.ONE), new ECPoint(BigInteger.ONE, BigInteger.ONE), order, 1) + ECPrivateKey priv = new TestECPrivateKey(params: spec) + def request = new DefaultSecureRequest(new byte[1], null, null, priv) + try { + it.digest(request) + } catch (InvalidKeyException expected) { + String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + + "${Bytes.bitsMsg(order.bitLength())}, but the '${it.getId()}' algorithm requires EC Keys with " + + "${Bytes.bitsMsg(it.orderBitLength)} per " + + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." as String + assertEquals msg, expected.getMessage() + } + } + } + + @Test + void testSignWithInvalidKeyFieldLength() { + def keypair = Jwts.SIG.ES256.keyPairBuilder().build() + def data = "foo".getBytes(StandardCharsets.UTF_8) + def req = new DefaultSecureRequest(data, null, null, keypair.private) + try { + Jwts.SIG.ES384.digest(req) + } catch (InvalidKeyException expected) { + String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + + "256 bits (32 bytes), but the 'ES384' algorithm requires EC Keys with " + + "384 bits (48 bytes) per " + + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testVerifyWithPrivateKey() { + byte[] data = 'foo'.getBytes(StandardCharsets.UTF_8) + algs().each { + def pair = it.keyPairBuilder().build() + def key = pair.getPrivate() + def signRequest = new DefaultSecureRequest(data, null, null, key) + byte[] signature = it.digest(signRequest) + def verifyRequest = new DefaultVerifySecureDigestRequest(data, null, null, key, signature) + try { + it.verify(verifyRequest) + } catch (InvalidKeyException e) { + String msg = "${it.getId()} verification keys must be PublicKeys (implement " + + "${PublicKey.class.name}). Provided key type: ${key.class.name}." + assertEquals msg, e.getMessage() + } + } + } + + @Test + void testVerifyWithWeakKey() { + algs().each { + BigInteger order = BigInteger.ONE + ECParameterSpec spec = new ECParameterSpec(new EllipticCurve(new TestECField(), BigInteger.ONE, BigInteger.ONE), new ECPoint(BigInteger.ONE, BigInteger.ONE), order, 1) + ECPublicKey pub = new TestECPublicKey(params: spec) + def request = new DefaultVerifySecureDigestRequest(new byte[1], null, null, pub, new byte[1]) + try { + it.verify(request) + } catch (InvalidKeyException expected) { + String msg = "The provided Elliptic Curve verification key's size (aka Order bit length) is " + + "${Bytes.bitsMsg(order.bitLength())}, but the '${it.getId()}' algorithm requires EC Keys with " + + "${Bytes.bitsMsg(it.orderBitLength)} per " + + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." as String + assertEquals msg, expected.getMessage() + } + } + } + + @Test + void invalidDERSignatureToJoseFormatTest() { + def verify = { byte[] signature -> + try { + EcSignatureAlgorithm.transcodeDERToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + def signature = new byte[257] + Randoms.secureRandom().nextBytes(signature) + //invalid type + signature[0] = 34 + verify(signature) + def shortSignature = new byte[7] + Randoms.secureRandom().nextBytes(shortSignature) + verify(shortSignature) + signature[0] = 48 +// signature[1] = 0x81 + signature[1] = -10 + verify(signature) + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureTest() { + try { + def signature = Decoders.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EcSignatureAlgorithm.transcodeDERToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { + try { + def signature = Decoders.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EcSignatureAlgorithm.transcodeDERToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { + try { + def signature = Decoders.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EcSignatureAlgorithm.transcodeDERToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatLengthTest() { + try { + def signature = Decoders.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") + EcSignatureAlgorithm.transcodeDERToConcat(signature, 132) + fail() + } catch (JwtException expected) { + } + } + + @Test + void invalidECDSASignatureFormatTest() { + try { + def signature = new byte[257] + Randoms.secureRandom().nextBytes(signature) + EcSignatureAlgorithm.transcodeConcatToDER(signature) + fail() + } catch (JwtException e) { + assertEquals 'Invalid ECDSA signature format.', e.message + } + } + + @Test + void edgeCaseSignatureLengthTest() { + def signature = new byte[1] + EcSignatureAlgorithm.transcodeConcatToDER(signature) + } + + @Test + void testPaddedSignatureToDER() { + def signature = new byte[32] + Randoms.secureRandom().nextBytes(signature) + signature[0] = 0 as byte + EcSignatureAlgorithm.transcodeConcatToDER(signature) //no exception + } + + @Test + void ecdsaSignatureCompatTest() { + def fact = KeyFactory.getInstance("EC") + def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" + def pub = fact.generatePublic(new X509EncodedKeySpec(Decoders.BASE64.decode(publicKey))) + def alg = Jwts.SIG.ES512 + def verifier = { String token -> + def signatureStart = token.lastIndexOf('.') + def withoutSignature = token.substring(0, signatureStart) + def data = withoutSignature.getBytes("US-ASCII") + def signature = Decoders.BASE64URL.decode(token.substring(signatureStart + 1)) + assertTrue "Signature do not match that of other implementations", alg.verify(new DefaultVerifySecureDigestRequest(data, null, null, pub, signature)) + } + //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 + verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") + //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 + verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") + } + + @Test + void verifySwarmTest() { + algs().each { alg -> + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = alg.keyPairBuilder().build() + assertNotNull keypair + assertTrue keypair.getPublic() instanceof ECPublicKey + assertTrue keypair.getPrivate() instanceof ECPrivateKey + def data = withoutSignature.getBytes("US-ASCII") + def signature = alg.digest(new DefaultSecureRequest<>(data, null, null, keypair.private)) + assertTrue alg.verify(new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, signature)) + } + } + + // ===================== Begin imported EllipticCurveSignerTest test cases ============================== + + /* + + @Test + void testDoSignWithInvalidKeyException() { + + SignatureAlgorithm alg = SignatureAlgorithm.ES256 + + KeyPair kp = Keys.keyPairFor(alg) + PrivateKey privateKey = kp.getPrivate() + + String msg = 'foo' + final java.security.InvalidKeyException ex = new java.security.InvalidKeyException(msg) + + def signer = new EllipticCurveSigner(alg, privateKey) { + @Override + protected byte[] doSign(byte[] data) throws java.security.InvalidKeyException, java.security.SignatureException { + throw ex + } + } + + byte[] bytes = new byte[16] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + + try { + signer.digest(bytes) + fail(); + } catch (io.jsonwebtoken.security.SignatureException se) { + assertEquals se.message, 'Invalid Elliptic Curve PrivateKey. ' + msg + assertSame se.cause, ex + } + } + + @Test + void testDoSignWithJoseSignatureFormatException() { + + SignatureAlgorithm alg = SignatureAlgorithm.ES256 + KeyPair kp = EllipticCurveProvider.generateKeyPair(alg) + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + String msg = 'foo' + final JwtException ex = new JwtException(msg) + + def signer = new EllipticCurveSigner(alg, privateKey) { + @Override + protected byte[] doSign(byte[] data) throws java.security.InvalidKeyException, java.security.SignatureException, JwtException { + throw ex + } + } + + byte[] bytes = new byte[16] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + + try { + signer.digest(bytes) + fail(); + } catch (io.jsonwebtoken.security.SignatureException se) { + assertEquals se.message, 'Unable to convert signature to JOSE format. ' + msg + assertSame se.cause, ex + } + } + + @Test + void testDoSignWithJdkSignatureException() { + + SignatureAlgorithm alg = SignatureAlgorithm.ES256 + KeyPair kp = EllipticCurveProvider.generateKeyPair(alg) + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + String msg = 'foo' + final java.security.SignatureException ex = new java.security.SignatureException(msg) + + def signer = new EllipticCurveSigner(alg, privateKey) { + @Override + protected byte[] doSign(byte[] data) throws java.security.InvalidKeyException, java.security.SignatureException { + throw ex + } + } + + byte[] bytes = new byte[16] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + + try { + signer.digest(bytes) + fail(); + } catch (io.jsonwebtoken.security.SignatureException se) { + assertEquals se.message, 'Unable to calculate signature using Elliptic Curve PrivateKey. ' + msg + assertSame se.cause, ex + } + } + + @Test + void testDoVerifyWithInvalidKeyException() { + + String msg = 'foo' + final java.security.InvalidKeyException ex = new java.security.InvalidKeyException(msg) + def alg = SignatureAlgorithm.ES512 + def keypair = EllipticCurveProvider.generateKeyPair(alg) + + def v = new EllipticCurveSignatureValidator(alg, EllipticCurveProvider.generateKeyPair(alg).public) { + @Override + protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws java.security.InvalidKeyException, java.security.SignatureException { + throw ex; + } + } + + byte[] data = new byte[32] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(data) + + byte[] signature = new EllipticCurveSigner(alg, keypair.getPrivate()).digest(data) + + try { + v.isValid(data, signature) + fail(); + } catch (io.jsonwebtoken.security.SignatureException se) { + assertEquals se.message, 'Unable to verify Elliptic Curve signature using configured ECPublicKey. ' + msg + assertSame se.cause, ex + } + } + + */ + + @Test + void ecdsaSignatureInteropTest() { + def fact = KeyFactory.getInstance("EC") + def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" + def pub = fact.generatePublic(new X509EncodedKeySpec(Decoders.BASE64.decode(publicKey))) + def alg = Jwts.SIG.ES512 + def verifier = { String token -> + def signatureStart = token.lastIndexOf('.') + def withoutSignature = token.substring(0, signatureStart) + def signature = token.substring(signatureStart + 1) + + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + def sigBytes = Decoders.BASE64URL.decode(signature) + def request = new DefaultVerifySecureDigestRequest(data, null, null, pub, sigBytes) + assert alg.verify(request), "Signature do not match that of other implementations" + } + //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 + verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") + //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 + verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") + } + + @Test + // asserts guard for JVM security bug CVE-2022-21449: + void legacySignatureCompatDefaultTest() { + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = Jwts.SIG.ES512 + def keypair = alg.keyPairBuilder().build() + def signature = Signature.getInstance(alg.jcaName as String) + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + signature.initSign(keypair.private) + signature.update(data) + def signed = signature.sign() + def request = new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, signed) + try { + alg.verify(request) + fail() + } catch (SignatureException expected) { + String signedBytesString = Bytes.bytesMsg(signed.length) + String msg = "Unable to verify Elliptic Curve signature using provided ECPublicKey: Provided " + + "signature is $signedBytesString but ES512 signatures must be exactly 1056 bits (132 bytes) " + + "per [RFC 7518, Section 3.4 (validation)]" + + "(https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." as String + assertEquals msg, expected.getMessage() + } + } + + @Test + void legacySignatureCompatWhenEnabledTest() { + try { + System.setProperty(EcSignatureAlgorithm.DER_ENCODING_SYS_PROPERTY_NAME, 'true') + + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = Jwts.SIG.ES512 + def keypair = alg.keyPairBuilder().build() + def signature = Signature.getInstance(alg.jcaName as String) + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + signature.initSign(keypair.private) + signature.update(data) + def signed = signature.sign() + def request = new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, signed) + assertTrue alg.verify(request) + } finally { + System.clearProperty(EcSignatureAlgorithm.DER_ENCODING_SYS_PROPERTY_NAME) + } + } + + @Test + // asserts guard for JVM security bug CVE-2022-21449: + void testVerifySignatureAllZeros() { + byte[] forgedSig = new byte[64] + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = Jwts.SIG.ES256 + def keypair = alg.keyPairBuilder().build() + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + def request = new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, forgedSig) + assertFalse alg.verify(request) + } + + @Test + // asserts guard for JVM security bug CVE-2022-21449: + void testVerifySignatureRZero() { + byte[] r = new byte[32] + byte[] s = new byte[32]; Arrays.fill(s, Byte.MAX_VALUE) + byte[] sig = new byte[r.length + s.length] + System.arraycopy(r, 0, sig, 0, r.length) + System.arraycopy(s, 0, sig, r.length, s.length) + + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = Jwts.SIG.ES256 + def keypair = alg.keyPairBuilder().build() + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + def request = new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, sig) + assertFalse alg.verify(request) + } + + @Test + // asserts guard for JVM security bug CVE-2022-21449: + void testVerifySignatureSZero() { + byte[] r = new byte[32]; Arrays.fill(r, Byte.MAX_VALUE) + byte[] s = new byte[32] + byte[] sig = new byte[r.length + s.length] + System.arraycopy(r, 0, sig, 0, r.length) + System.arraycopy(s, 0, sig, r.length, s.length) + + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = Jwts.SIG.ES256 + def keypair = alg.keyPairBuilder().build() + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + def request = new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, sig) + assertFalse alg.verify(request) + } + + @Test + // asserts guard for JVM security bug CVE-2022-21449: + void ecdsaInvalidSignatureValuesTest() { + def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" + def alg = Jwts.SIG.ES256 + def keypair = alg.keyPairBuilder().build() + def data = withoutSignature.getBytes(StandardCharsets.US_ASCII) + def invalidSignature = Decoders.BASE64URL.decode(invalidEncodedSignature) + def request = new DefaultVerifySecureDigestRequest(data, null, null, keypair.public, invalidSignature) + assertFalse("Forged signature must not be considered valid.", alg.verify(request)) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy new file mode 100644 index 00000000..2247daba --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcdhKeyAlgorithmTest.groovy @@ -0,0 +1,224 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.security.DecryptionKeyRequest +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.PrivateKey +import java.security.PublicKey +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +/** + * The {@link EcdhKeyAlgorithm} class is mostly tested already in RFC Appendix tests, so this class + * adds in tests for assertions/conditionals that aren't as easily tested elsewhere. + */ +class EcdhKeyAlgorithmTest { + + @Test + void testEdwardsEncryptionWithRequestProvider() { + def alg = new EcdhKeyAlgorithm() + PublicKey encKey = TestKeys.X25519.pair.public as PublicKey + def header = new DefaultJweHeader() + def provider = Providers.findBouncyCastle(Conditions.TRUE) + def request = new DefaultKeyRequest(encKey, provider, null, header, Jwts.ENC.A128GCM) + def result = alg.getEncryptionKey(request) + assertNotNull result.getKey() + } + + @Test + void testEdwardsDecryptionWithRequestProvider() { + def alg = new EcdhKeyAlgorithm() + def enc = Jwts.ENC.A128GCM + PublicKey encKey = TestKeys.X25519.pair.public as PublicKey + PrivateKey decKey = TestKeys.X25519.pair.private as PrivateKey + def header = new DefaultJweHeader() + def provider = Providers.findBouncyCastle(Conditions.TRUE) + + // encrypt + def request = new DefaultKeyRequest(encKey, provider, null, header, enc) + def result = alg.getEncryptionKey(request) + def cek = result.getKey() + def cekCiphertext = result.getPayload() + + def decRequest = new DefaultDecryptionKeyRequest(cekCiphertext, provider, null, header, enc, decKey) + def resultCek = alg.getDecryptionKey(decRequest) + assertEquals(cek, resultCek) + } + + @Test + void testDecryptionWithMissingEcPublicJwk() { + + def alg = new EcdhKeyAlgorithm() + ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey + + def header = new DefaultJweHeader() + + DecryptionKeyRequest req = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, decryptionKey) + + try { + alg.getDecryptionKey(req) + fail() + } catch (MalformedJwtException expected) { + String msg = "JWE header is missing required 'epk' (Ephemeral Public Key) value." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testDecryptionWithEcPublicJwkOnInvalidCurve() { + + def alg = new EcdhKeyAlgorithm() + ECPrivateKey decryptionKey = TestKeys.ES256.pair.private as ECPrivateKey // Expected curve for this is P-256 + + def header = new DefaultJweHeader() + // This uses curve P-384 instead, does not match private key, so it's unexpected: + def jwk = Jwks.builder().forKey(TestKeys.ES384.pair.public as ECPublicKey).build() + header.put('epk', jwk) + + DecryptionKeyRequest req = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, decryptionKey) + + try { + alg.getDecryptionKey(req) + fail() + } catch (InvalidKeyException expected) { + assertEquals("JWE Header 'epk' (Ephemeral Public Key) value does not represent a point on the expected curve.", expected.getMessage()) + } + } + + + @Test + void testEncryptionWithInvalidPublicKey() { + def alg = new EcdhKeyAlgorithm() + PublicKey encKey = TestKeys.RS256.pair.public as PublicKey // not an elliptic curve key, must fail + def header = new DefaultJweHeader() + def request = new DefaultKeyRequest(encKey, null, null, header, Jwts.ENC.A128GCM) + try { + alg.getEncryptionKey(request) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Key Encryption Key must be a java.security.interfaces.ECKey or valid Edwards Curve ' + + 'PublicKey instance. Cause: sun.security.rsa.RSAPublicKeyImpl with algorithm \'RSA\' is not a ' + + 'recognized Edwards Curve key.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testDecryptionWithInvalidPrivateKey() { + def alg = new EcdhKeyAlgorithm() + PrivateKey key = TestKeys.RS256.pair.private as PrivateKey // not an elliptic curve key, must fail + def header = new DefaultJweHeader() + header.put('epk', Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build()) + def request = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, key) + try { + alg.getDecryptionKey(request) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Key Decryption Key must be a java.security.interfaces.ECKey or valid Edwards Curve ' + + 'PrivateKey instance. Cause: sun.security.rsa.RSAPrivateCrtKeyImpl with algorithm \'RSA\' is ' + + 'not a recognized Edwards Curve key.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testDecryptionWithoutEpk() { + def alg = new EcdhKeyAlgorithm() + PrivateKey key = TestKeys.ES256.pair.private as PrivateKey // valid key + def header = new DefaultJweHeader() // no 'epk' value + def request = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, key) + try { + alg.getDecryptionKey(request) + fail() + } catch (MalformedJwtException expected) { + String msg = 'JWE header is missing required \'epk\' (Ephemeral Public Key) value.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testECDecryptionWithNonECEpk() { + def alg = new EcdhKeyAlgorithm() + PrivateKey key = TestKeys.ES256.pair.private as PrivateKey // valid key + def header = new DefaultJweHeader() + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() // invalid epk + header.put('epk', jwk) + def request = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, key) + try { + alg.getDecryptionKey(request) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'JWE Header \'epk\' (Ephemeral Public Key) value is not a supported Elliptic Curve Public ' + + 'JWK. Value: {kty=RSA, n=zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVE' + + 'z7m298ENqNGqPk2Nv3pBJ_XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqT' + + 'xj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6N' + + 'dFxh6T_pMXpkf8d81fzVo4ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, e=AQAB}' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEdwardsDecryptionWithNonEdwardsEpk() { + def alg = new EcdhKeyAlgorithm() + PrivateKey key = TestKeys.X25519.pair.private as PrivateKey // valid key + def header = new DefaultJweHeader() + def jwk = Jwks.builder().forKey(TestKeys.RS256.pair.public as RSAPublicKey).build() // invalid epk + header.put('epk', jwk) + def request = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, key) + try { + alg.getDecryptionKey(request) + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'JWE Header \'epk\' (Ephemeral Public Key) value is not a supported Elliptic Curve Public ' + + 'JWK. Value: {kty=RSA, n=zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVE' + + 'z7m298ENqNGqPk2Nv3pBJ_XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqT' + + 'xj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6N' + + 'dFxh6T_pMXpkf8d81fzVo4ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, e=AQAB}' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testEdwardsDecryptionWithEpkOnDifferentCurve() { + def alg = new EcdhKeyAlgorithm() + PrivateKey key = TestKeys.X25519.pair.private as PrivateKey // valid key + def header = new DefaultJweHeader() + def jwk = Jwks.builder().forKey(TestKeys.X448.pair.public as PublicKey).build() // epk is not on X25519 + header.put('epk', jwk) + def request = new DefaultDecryptionKeyRequest('test'.getBytes(), null, null, header, Jwts.ENC.A128GCM, key) + try { + alg.getDecryptionKey(request) + fail() + } catch (InvalidKeyException expected) { + String msg = 'JWE Header \'epk\' (Ephemeral Public Key) value does not represent a point on the ' + + 'expected curve. Value: {kty=OKP, crv=X448, ' + + 'x=_XW37ksNpY3J7qglqWh56nZP3WgdrJlMtxPaplYn4zkPBZKanWlk2gR-m1xO2NXAOL3JZhHQBCc}' + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy new file mode 100644 index 00000000..ff9936c0 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy @@ -0,0 +1,146 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.security.SignatureAlgorithm +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import java.security.PrivateKey +import java.security.PublicKey + +import static org.junit.Assert.* + +class EdSignatureAlgorithmTest { + + static List algs = [Jwts.SIG.EdDSA, Jwts.SIG.Ed25519, Jwts.SIG.Ed448] as List + + @Test + void testJcaName() { + assertEquals Jwts.SIG.EdDSA.getId(), Jwts.SIG.EdDSA.getJcaName() + assertEquals EdwardsCurve.Ed25519.getId(), Jwts.SIG.Ed25519.getJcaName() + assertEquals EdwardsCurve.Ed448.getId(), Jwts.SIG.Ed448.getJcaName() + } + + @Test + void testId() { + //There is only one signature algorithm ID defined for Edwards curve keys per + // https://www.rfc-editor.org/rfc/rfc8037#section-3.1 and + // https://www.rfc-editor.org/rfc/rfc8037#section-5 + // + // As such, the Ed25519 and Ed448 SignatureAlgorithm instances _must_ reflect the same ID since that's the + // only one recognized by the spec. They are effectively just aliases of EdDSA but have the added + // functionality of generating Ed25519 and Ed448 keys, that's the only difference. + for (EdSignatureAlgorithm alg : algs) { + assertEquals Jwts.SIG.EdDSA.getId(), alg.getId() // all aliases of EdDSA per the RFC spec + } + } + + @Test + void testKeyPairBuilder() { + algs.each { + def pair = it.keyPairBuilder().build() + assertNotNull pair.public + assertTrue pair.public instanceof PublicKey + String alg = pair.public.getAlgorithm() + assertTrue Jwts.SIG.EdDSA.getId().equals(alg) || alg.equals(it.preferredCurve.getId()) + + alg = pair.private.getAlgorithm() + assertTrue Jwts.SIG.EdDSA.getId().equals(alg) || alg.equals(it.preferredCurve.getId()) + } + } + + /** + * Likely when keys are from an HSM or PKCS key store + */ + @Test + void testGetAlgorithmJcaNameWhenCantFindCurve() { + def key = new TestKey(algorithm: 'foo') + algs.each { + def payload = [0x00] as byte[] + def req = new DefaultSecureRequest(payload, null , null, key) + assertEquals it.getJcaName(), it.getJcaName(req) + } + } + + @Test + void testEd25519SigVerifyWithEd448() { + testIncorrectVerificationKey(Jwts.SIG.Ed25519, TestKeys.Ed25519.pair.private, TestKeys.Ed448.pair.public) + } + + @Test + void testEd25519SigVerifyWithX25519() { + testInvalidVerificationKey(Jwts.SIG.Ed25519, TestKeys.Ed25519.pair.private, TestKeys.X25519.pair.public) + } + + @Test + void testEd25519SigVerifyWithX448() { + testInvalidVerificationKey(Jwts.SIG.Ed25519, TestKeys.Ed25519.pair.private, TestKeys.X448.pair.public) + } + + @Test + void testEd448SigVerifyWithEd25519() { + testIncorrectVerificationKey(Jwts.SIG.Ed448, TestKeys.Ed448.pair.private, TestKeys.Ed25519.pair.public) + } + + @Test + void testEd448SigVerifyWithX25519() { + testInvalidVerificationKey(Jwts.SIG.Ed448, TestKeys.Ed448.pair.private, TestKeys.X25519.pair.public) + } + + @Test + void testEd448SigVerifyWithX448() { + testInvalidVerificationKey(Jwts.SIG.Ed448, TestKeys.Ed448.pair.private, TestKeys.X448.pair.public) + } + + static void testIncorrectVerificationKey(SignatureAlgorithm alg, PrivateKey priv, PublicKey pub) { + try { + testSig(alg, priv, pub) + fail() + } catch (SignatureException expected) { + // SignatureException message can differ depending on JDK version and if BC is enabled or not: + // BC Provider signature.verify() will just return false, but SunEC provider signature.verify() throws an + // exception with its own message. As a result, we should always get a SignatureException, but we need + // to check the message for either scenario depending on the JVM version running the tests: + String exMsg = expected.getMessage() + String expectedMsg = 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.' + String expectedMsg2 = "Unable to verify EdDSA signature with JCA algorithm 'EdDSA' using key {${pub}}: ${expected.getCause()?.getMessage()}" + assertTrue exMsg.equals(expectedMsg) || exMsg.equals(expectedMsg2) + } + } + + static void testInvalidVerificationKey(SignatureAlgorithm alg, PrivateKey priv, PublicKey pub) { + try { + testSig(alg, priv, pub) + fail() + } catch (UnsupportedJwtException expected) { + def cause = expected.getCause() + def keyCurve = EdwardsCurve.forKey(pub) + String expectedMsg = "${keyCurve.getId()} keys may not be used with EdDSA digital signatures per https://www.rfc-editor.org/rfc/rfc8037#section-3.2" + assertEquals expectedMsg, cause.getMessage() + } + } + + static void testSig(SignatureAlgorithm alg, PrivateKey signing, PublicKey verification) { + String jwt = Jwts.builder().setIssuer('me').setAudience('you').signWith(signing, alg).compact() + def token = Jwts.parserBuilder().verifyWith(verification).build().parseClaimsJws(jwt) + assertEquals([alg: alg.getId()], token.header) + assertEquals 'me', token.getPayload().getIssuer() + assertEquals 'you', token.getPayload().getAudience() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdwardsCurveTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdwardsCurveTest.groovy new file mode 100644 index 00000000..cd156800 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdwardsCurveTest.groovy @@ -0,0 +1,371 @@ +/* + * 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.security + +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.lang.Function +import io.jsonwebtoken.impl.lang.Functions +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.spec.AlgorithmParameterSpec +import java.security.spec.ECGenParameterSpec +import java.security.spec.KeySpec + +import static org.junit.Assert.* + +class EdwardsCurveTest { + + static final Collection curves = EdwardsCurve.VALUES + + @SuppressWarnings('GroovyResultOfObjectAllocationIgnored') + @Test + void testInvalidOidTerminalNode() { + try { + new EdwardsCurve('foo', 200) + fail() + } catch (IllegalArgumentException iae) { + String expected = 'Invalid Edwards Curve ASN.1 OID terminal node value' + assertEquals expected, iae.getMessage() + } + } + + @Test + void testKeyBitLength() { + assertEquals(256, EdwardsCurve.X25519.getKeyBitLength()) + assertEquals(256, EdwardsCurve.Ed25519.getKeyBitLength()) + assertEquals(448, EdwardsCurve.X448.getKeyBitLength()) + assertEquals(456, EdwardsCurve.Ed448.getKeyBitLength()) + } + + @Test + void testIsEdwardsNullKey() { + assertFalse EdwardsCurve.isEdwards(null) + } + + @Test + void testFindByNullKey() { + assertNull EdwardsCurve.findByKey(null) + } + + @Test + void testForKeyNonEdwards() { + def alg = 'foo' + def key = new TestKey(algorithm: alg) + try { + EdwardsCurve.forKey(key) + } catch (UnsupportedKeyException uke) { + String msg = "${key.getClass().getName()} with algorithm '${alg}' is not a recognized Edwards Curve key." + assertEquals msg, uke.getMessage() + } + } + + @Test + void testFindByKeyUsingEncoding() { + curves.each { + def pair = TestKeys.forCurve(it).pair + def key = new TestKey(algorithm: 'foo', encoded: pair.public.getEncoded()) + def found = EdwardsCurve.findByKey(key) + assertEquals(it, found) + } + } + + @Test + void testFindByKeyUsingInvalidEncoding() { + curves.each { + byte[] encoded = new byte[it.keyBitLength / 8] + def key = new TestKey(algorithm: 'foo', encoded: encoded) + assertNull EdwardsCurve.findByKey(key) + } + } + + @Test + void testFindByKeyUsingMalformedEncoding() { + curves.each { + byte[] encoded = EdwardsCurve.DER_OID_PREFIX // just the prefix isn't enough + def key = new TestKey(algorithm: 'foo', encoded: encoded) + assertNull EdwardsCurve.findByKey(key) + } + } + + @Test + void testToPrivateKey() { + curves.each { + def pair = TestKeys.forCurve(it).pair + def key = pair.getPrivate() + def d = it.getKeyMaterial(key) + def result = it.toPrivateKey(d, it.getProvider()) + assertEquals(key, result) + } + } + + @Test + void testToPublicKey() { + curves.each { + def pair = TestKeys.forCurve(it).pair + def key = pair.getPublic() + def x = it.getKeyMaterial(key) + def result = it.toPublicKey(x, it.getProvider()) + assertEquals(key, result) + } + } + + @Test + void testToPrivateKeyInvalidLength() { + curves.each { + byte[] d = new byte[it.encodedKeyByteLength + 1] // more than required + Randoms.secureRandom().nextBytes(d) + try { + it.toPrivateKey(d, it.getProvider()) + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.id} encoded PrivateKey length. Should be " + + "${Bytes.bitsMsg(it.keyBitLength)}, found ${Bytes.bytesMsg(d.length)}." + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testToPublicKeyInvalidLength() { + curves.each { + byte[] x = new byte[it.encodedKeyByteLength - 1] // less than required + Randoms.secureRandom().nextBytes(x) + try { + it.toPublicKey(x, it.getProvider()) + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.id} encoded PublicKey length. Should be " + + "${Bytes.bitsMsg(it.keyBitLength)}, found ${Bytes.bytesMsg(x.length)}." + assertEquals msg, ike.getMessage() + } + } + } + + /** + * Ensures that if a DER NULL terminates the OID in the encoded key, the null tag is skipped. This occurs in + * some SunCE key encodings. + */ + @Test + void testGetKeyMaterialWithOidNullTerminator() { + byte[] DER_NULL = [0x05, 0x00] as byte[] + curves.each { it -> + + byte[] x = new byte[it.encodedKeyByteLength] + Randoms.secureRandom().nextBytes(x) + + byte[] encoded = Bytes.concat( + [0x30, it.encodedKeyByteLength + 10 + DER_NULL.length, 0x30, 0x05] as byte[], + it.DER_OID, + DER_NULL, // this should be skipped when getting key material + [0x03, it.encodedKeyByteLength + 1, 0x00] as byte[], + x + ) + + def key = new TestKey(encoded: encoded) + byte[] material = it.getKeyMaterial(key) + assertArrayEquals(x, material) + } + } + + @Test + void testGetKeyMaterialWithMissingEncodedBytes() { + def key = new TestKey(algorithm: 'foo') + curves.each { + try { + it.getKeyMaterial(key) + fail() + } catch (UnsupportedKeyException uke) { + String msg = "${key.getClass().getName()} encoded bytes cannot be null or empty." + assertEquals msg, uke.getMessage() + } + } + } + + @Test + void testGetKeyMaterialInvalidKeyEncoding() { + byte[] encoded = new byte[30] + Randoms.secureRandom().nextBytes(encoded) + //ensure random generator doesn't put in a byte that would cause other logic checks (0x03, 0x04, 0x05) + encoded[0] = 0x20 // anything other than 0x03, 0x04, 0x05 + def key = new TestKey(encoded: encoded) + curves.each { + try { + it.getKeyMaterial(key) + fail() + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.getId()} DER encoding: Missing or incorrect algorithm OID." as String + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testGetKeyMaterialInvalidKeyLength() { + byte[] encoded = new byte[30] + Randoms.secureRandom().nextBytes(encoded) + //ensure random generator doesn't put in a byte that would cause other logic checks (0x03, 0x04, 0x05) + encoded[0] = 0x20 // anything other than 0x03, 0x04, 0x05 + curves.each { + // prefix it with the OID to make it look valid: + encoded = Bytes.concat(it.DER_OID, encoded) + def key = new TestKey(encoded: encoded) + try { + it.getKeyMaterial(key) + fail() + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.getId()} DER encoding: Invalid key length." as String + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testPublicKeyMaterialInvalidBitSequence() { + int size = 0 + curves.each { + try { + size = it.encodedKeyByteLength + byte[] keyBytes = new byte[size] + Randoms.secureRandom().nextBytes(keyBytes) + byte[] encoded = Bytes.concat(it.PUBLIC_KEY_DER_PREFIX, keyBytes) + encoded[11] = 0x01 // should always be zero + def key = new TestKey(encoded: encoded) + it.getKeyMaterial(key) + fail() + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.getId()} DER encoding: BIT STREAM should not indicate unused bytes." as String + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testPrivateKeyMaterialInvalidOctetSequence() { + int size = 0 + curves.each { + try { + size = it.encodedKeyByteLength + byte[] keyBytes = new byte[size] + Randoms.secureRandom().nextBytes(keyBytes) + byte[] encoded = Bytes.concat(it.PRIVATE_KEY_DER_PREFIX, keyBytes) + encoded[14] = 0x0F // should always be 0x04 (DER SEQUENCE tag) + def key = new TestKey(encoded: encoded) + it.getKeyMaterial(key) + fail() + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.getId()} DER encoding: Invalid key length." as String + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testGetKeyMaterialTooShort() { + int size = 0 + curves.each { + try { + size = it.encodedKeyByteLength - 1 // one less than required + byte[] keyBytes = new byte[size] + Randoms.secureRandom().nextBytes(keyBytes) + byte[] encoded = Bytes.concat(it.PUBLIC_KEY_DER_PREFIX, keyBytes) + encoded[10] = (byte) (size + 1) // DER size value (zero byte + key bytes) + def key = new TestKey(encoded: encoded) + it.getKeyMaterial(key) + fail() + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.getId()} DER encoding: Invalid key length." as String + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testGetKeyMaterialTooLong() { + int size = 0 + curves.each { + try { + size = it.encodedKeyByteLength + 1 // one less than required + byte[] keyBytes = new byte[size] + Randoms.secureRandom().nextBytes(keyBytes) + byte[] encoded = Bytes.concat(it.PUBLIC_KEY_DER_PREFIX, keyBytes) + encoded[10] = (byte) (size + 1) // DER size value (zero byte + key bytes) + def key = new TestKey(encoded: encoded) + it.getKeyMaterial(key) + fail() + } catch (InvalidKeyException ike) { + String msg = "Invalid ${it.getId()} DER encoding: Invalid key length." as String + assertEquals msg, ike.getMessage() + } + } + } + + @Test + void testParamKeySpecFactoryWithNullSpec() { + def fn = EdwardsCurve.paramKeySpecFactory(null, true) + assertSame Functions.forNull(), fn + } + + @Test + void testXecParamKeySpecFactory() { + AlgorithmParameterSpec spec = new ECGenParameterSpec('foo') // any impl will do for this test + def fn = EdwardsCurve.paramKeySpecFactory(spec, false) as EdwardsCurve.ParameterizedKeySpecFactory + assertSame spec, fn.params + assertSame EdwardsCurve.XEC_PRIV_KEY_SPEC_CTOR, fn.keySpecFactory + } + + @Test + void testEdEcParamKeySpecFactory() { + AlgorithmParameterSpec spec = new ECGenParameterSpec('foo') // any impl will do for this test + def fn = EdwardsCurve.paramKeySpecFactory(spec, true) as EdwardsCurve.ParameterizedKeySpecFactory + assertSame spec, fn.params + assertSame EdwardsCurve.EDEC_PRIV_KEY_SPEC_CTOR, fn.keySpecFactory + } + + @Test + void testParamKeySpecFactoryInvocation() { + AlgorithmParameterSpec spec = new ECGenParameterSpec('foo') // any impl will do for this test + KeySpec keySpec = new PasswordSpec("foo".toCharArray()) // any KeySpec impl will do + + byte[] d = new byte[32] + Randoms.secureRandom().nextBytes(d) + + def keySpecFn = new Function() { + @Override + KeySpec apply(Object o) { + assertTrue o instanceof Object[] + Object[] args = (Object[]) o + assertSame spec, args[0] + assertSame d, args[1] + return keySpec // simulate a creation + } + } + + def fn = new EdwardsCurve.ParameterizedKeySpecFactory(spec, keySpecFn) + def result = fn.apply(d) + assertSame keySpec, result + } + + @Test + void testDerivePublicKeyFromPrivateKey() { + for(def curve : EdwardsCurve.VALUES) { + def pair = curve.keyPairBuilder().build() // generate a standard key pair using the JCA APIs + def pubKey = pair.getPublic() + def derivedPubKey = EdwardsCurve.derivePublic(pair.getPrivate()) + // ensure our derived key matches the original JCA one: + assertEquals(pubKey, derivedPubKey) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdwardsPublicKeyDeriverTest.groovy similarity index 52% rename from impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/EdwardsPublicKeyDeriverTest.groovy index 5d33fd74..3118451c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdwardsPublicKeyDeriverTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * 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. @@ -13,28 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto +package io.jsonwebtoken.impl.security -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.UnsupportedKeyException import org.junit.Test import static org.junit.Assert.assertEquals import static org.junit.Assert.fail -class DefaultSignerFactoryTest { +class EdwardsPublicKeyDeriverTest { @Test - void testCreateSignerWithNoneAlgorithm() { - - def factory = new DefaultSignerFactory(); - + void testDeriveWithNonEdwardsKey() { + def rsaPrivKey = Jwts.SIG.RS256.keyPairBuilder().build().getPrivate() try { - factory.createSigner(SignatureAlgorithm.NONE, Keys.secretKeyFor(SignatureAlgorithm.HS256)) - fail(); - } catch (IllegalArgumentException iae) { - assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." + EdwardsPublicKeyDeriver.INSTANCE.apply(rsaPrivKey) + fail() + } catch (UnsupportedKeyException uke) { + String expectedMsg = "Unable to derive Edwards-curve PublicKey for specified PrivateKey: ${KeysBridge.toString(rsaPrivKey)}" + assertEquals(expectedMsg, uke.getMessage()) } } - } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy new file mode 100644 index 00000000..343b97f6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy @@ -0,0 +1,46 @@ +/* + * 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.security + +import io.jsonwebtoken.security.SecretKeyBuilder + +import javax.crypto.SecretKey +import java.security.Provider +import java.security.SecureRandom + +class FixedSecretKeyBuilder implements SecretKeyBuilder { + + final SecretKey key + + FixedSecretKeyBuilder(SecretKey key) { + this.key = key + } + + @Override + SecretKey build() { + return this.key + } + + @Override + SecretKeyBuilder setProvider(Provider provider) { + return this + } + + @Override + SecretKeyBuilder setRandom(SecureRandom random) { + return this + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy new file mode 100644 index 00000000..612e96bb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwts +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.fail + +/** + * @since JJWT_RELEASE_VERSION + */ +class GcmAesAeadAlgorithmTest { + + final byte[] K = + [0xb1, 0xa1, 0xf4, 0x80, 0x54, 0x8f, 0xe1, 0x73, 0x3f, 0xb4, 0x3, 0xff, 0x6b, 0x9a, 0xd4, 0xf6, + 0x8a, 0x7, 0x6e, 0x5b, 0x70, 0x2e, 0x22, 0x69, 0x2f, 0x82, 0xcb, 0x2e, 0x7a, 0xea, 0x40, 0xfc] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] P = "The true sign of intelligence is not knowledge but imagination.".getBytes("UTF-8") + + final byte[] IV = [0xe3, 0xc5, 0x75, 0xfc, 0x2, 0xdb, 0xe9, 0x44, 0xb4, 0xe1, 0x4d, 0xdb] as byte[] + + final byte[] AAD = + [0x65, 0x79, 0x4a, 0x68, 0x62, 0x47, 0x63, 0x69, 0x4f, 0x69, 0x4a, 0x53, 0x55, 0x30, 0x45, 0x74, + 0x54, 0x30, 0x46, 0x46, 0x55, 0x43, 0x49, 0x73, 0x49, 0x6d, 0x56, 0x75, 0x59, 0x79, 0x49, 0x36, + 0x49, 0x6b, 0x45, 0x79, 0x4e, 0x54, 0x5a, 0x48, 0x51, 0x30, 0x30, 0x69, 0x66, 0x51] as byte[] + + final byte[] E = + [0xe5, 0xec, 0xa6, 0xf1, 0x35, 0xbf, 0x73, 0xc4, 0xae, 0x2b, 0x49, 0x6d, 0x27, 0x7a, 0xe9, 0x60, + 0x8c, 0xce, 0x78, 0x34, 0x33, 0xed, 0x30, 0xb, 0xbe, 0xdb, 0xba, 0x50, 0x6f, 0x68, 0x32, 0x8e, + 0x2f, 0xa7, 0x3b, 0x3d, 0xb5, 0x7f, 0xc4, 0x15, 0x28, 0x52, 0xf2, 0x20, 0x7b, 0x8f, 0xa8, 0xe2, + 0x49, 0xd8, 0xb0, 0x90, 0x8a, 0xf7, 0x6a, 0x3c, 0x10, 0xcd, 0xa0, 0x6d, 0x40, 0x3f, 0xc0] as byte[] + + final byte[] T = + [0x5c, 0x50, 0x68, 0x31, 0x85, 0x19, 0xa1, 0xd7, 0xad, 0x65, 0xdb, 0xd3, 0x88, 0x5b, 0xd2, 0x91] as byte[] + + /** + * Test that reflects https://tools.ietf.org/html/rfc7516#appendix-A.1 + */ + @Test + void testEncryptionAndDecryption() { + + def alg = Jwts.ENC.A256GCM + + def req = new DefaultAeadRequest(P, null, null, KEY, AAD, IV) + + def result = alg.encrypt(req) + + byte[] ciphertext = result.getPayload() + byte[] tag = result.getDigest() + byte[] iv = result.getInitializationVector() + + assertArrayEquals E, ciphertext + assertArrayEquals T, tag + assertArrayEquals IV, iv //shouldn't have been altered + + // now test decryption: + def dreq = new DefaultAeadResult(null, null, ciphertext, KEY, AAD, tag, iv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() + assertArrayEquals(P, decryptionResult) + } + + @Test + void testInstantiationWithInvalidKeyLength() { + try { + new GcmAesAeadAlgorithm(5) + fail() + } catch (IllegalArgumentException ignored) { + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HashAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HashAlgorithmsTest.groovy new file mode 100644 index 00000000..e22dda04 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HashAlgorithmsTest.groovy @@ -0,0 +1,121 @@ +/* + * 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.security + +import io.jsonwebtoken.security.HashAlgorithm +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* + +class HashAlgorithmsTest { + + static boolean contains(HashAlgorithm alg) { + return Jwks.HASH.values().contains(alg) + } + + @Test + void testValues() { + assertEquals 6, Jwks.HASH.values().size() + assertTrue(contains(Jwks.HASH.SHA256)) // add more later + } + + @Test + void testForId() { + for (HashAlgorithm alg : Jwks.HASH.values()) { + assertSame alg, Jwks.HASH.get(alg.getId()) + } + } + + @Test + void testForIdCaseInsensitive() { + for (HashAlgorithm alg : Jwks.HASH.values()) { + assertSame alg, Jwks.HASH.get(alg.getId().toLowerCase()) + } + } + + @Test(expected = IllegalArgumentException) + void testForIdWithInvalidId() { + //unlike the 'find' paradigm, 'get' requires the value to exist + Jwks.HASH.get('invalid') + } + + @Test + void testFindById() { + for (HashAlgorithm alg : Jwks.HASH.values()) { + assertSame alg, Jwks.HASH.find(alg.getId()) + } + } + + @Test + void testFindByIdCaseInsensitive() { + for (HashAlgorithm alg : Jwks.HASH.values()) { + assertSame alg, Jwks.HASH.find(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull Jwks.HASH.find('invalid') + } + + static DefaultRequest request(String msg) { + byte[] data = msg.getBytes(StandardCharsets.UTF_8) + return new DefaultRequest(data, null, null) + } + + static void testSha(HashAlgorithm alg) { + String id = alg.getId() + int c = ('-' as char) as int + def digestLength = id.substring(id.lastIndexOf(c) + 1) as int + assertTrue alg.getJcaName().endsWith('' + digestLength) + def digest = alg.digest(request("hello")) + assertEquals digestLength, (digest.length * Byte.SIZE) + } + + @Test + void testSha256() { + testSha(Jwks.HASH.SHA256) + } + + @Test + void testSha384() { + testSha(Jwks.HASH.SHA384) + } + + @Test + void testSha512() { + testSha(Jwks.HASH.SHA512) + } + + @Test + void testSha3_256() { + testSha(Jwks.HASH.SHA3_256) + } + + @Test + void testSha3_384() { + testSha(Jwks.HASH.SHA3_384) + } + + @Test + void testSha3_512() { + testSha(Jwks.HASH.SHA3_512) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy new file mode 100644 index 00000000..4448831f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.security.AeadAlgorithm +import io.jsonwebtoken.security.SignatureException +import org.junit.Test + +import javax.crypto.SecretKey + +import static org.junit.Assert.assertEquals + +/** + * @since JJWT_RELEASE_VERSION + */ +class HmacAesAeadAlgorithmTest { + + @Test + void testKeyBitLength() { + // asserts that key lengths are double than what is usually expected for AES + // due to the encrypt-then-mac scheme requiring two separate keys + // (encrypt key is half of the generated key, mac key is the 2nd half of the generated key): + assertEquals 256, Jwts.ENC.A128CBC_HS256.getKeyBitLength() + assertEquals 384, Jwts.ENC.A192CBC_HS384.getKeyBitLength() + assertEquals 512, Jwts.ENC.A256CBC_HS512.getKeyBitLength() + } + + @Test + void testGenerateKey() { + def algs = [ + Jwts.ENC.A128CBC_HS256, + Jwts.ENC.A192CBC_HS384, + Jwts.ENC.A256CBC_HS512 + ] + for (AeadAlgorithm alg : algs) { + SecretKey key = alg.keyBuilder().build() + assertEquals alg.getKeyBitLength(), Bytes.bitLength(key.getEncoded()) + } + } + + @Test(expected = SignatureException) + void testDecryptWithInvalidTag() { + + def alg = Jwts.ENC.A128CBC_HS256 + + SecretKey key = alg.keyBuilder().build() + + def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8") + + def req = new DefaultAeadRequest(plaintext, null, null, key, null) + def result = alg.encrypt(req) + + def realTag = result.getDigest() + + //fake it: + def fakeTag = new byte[realTag.length] + Randoms.secureRandom().nextBytes(fakeTag) + + def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, null, fakeTag, result.getInitializationVector()) + alg.decrypt(dreq) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy similarity index 60% rename from impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy index 8636d053..9ac4ecef 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/Issue542Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Issue542Test.groovy @@ -1,15 +1,24 @@ -package io.jsonwebtoken.impl.crypto +/* + * Copyright (C) 2020 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.security import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo -import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.openssl.PEMParser -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import io.jsonwebtoken.security.SignatureAlgorithm import org.junit.Test -import java.nio.charset.StandardCharsets import java.security.PrivateKey import java.security.PublicKey @@ -31,52 +40,24 @@ class Issue542Test { private static String PS512_0_10_7 = 'eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJqb2UifQ.r6sisG-FVaMoIJacMSdYZLWFBVoT6bXmf3X3humLZqzoGfsRw3q9-wJ2oIiR4ua2L_mPnJqyPcjFWoXLUzw-URFSyQEAX_S2mWTBn7avCFsmJUh2fMkplG0ynbIHCqReRDl3moQGallbl-SYgArSRI2HbpVt05xsVbk3BmxB8N8buKbBPfUqwZMicRqNpHxoOc-IXaClc7y93gFNfGBMEwXn2nK_ZFXY03pMBL_MHVsJprPmtGfQw0ZZUv29zZbZTkRb6W6bRCi3jIP8sBMnYDqG3_Oyz9sF74IeOoD9sCpgAuRnrSAXhEb3tr1uBwyT__DOI1ZdT8QGFiRRNpUZDm7g4ub7njhXQ6ppkEY6kEKCCoxSq5sAh6EzZQgAfbpKNXy5VIu8s1nR-iJ8GDpeTcpLRhbX8havNzWjc-kSnU95_D5NFoaKfIjofKideVU46lUdCk-m7q8mOoFz8UEK1cXq3t7ay2jLG_sNvv7oZPe2TC4ovQGiQP0Mt446XBuIvyXSvygD3_ACpRSfpAqVoP7Ce98NkV2QCJxYNX1cZ4Zj4HrNoNWMx81TFoyU7RoUhj4tHcgBt_3_jbCO0OCejwswAFhwYRXP3jXeE2QhLaN1QJ7p97ly8WxjkBRac3I2WAeJhOM4CWhtgDmHAER9571MWp-7n4h4bnx9tXXfV7k' private static Map JWS_0_10_7_VALUES = [ - (SignatureAlgorithm.PS256): PS256_0_10_7, - (SignatureAlgorithm.PS384): PS384_0_10_7, - (SignatureAlgorithm.PS512): PS512_0_10_7 + (Jwts.SIG.PS256): PS256_0_10_7, + (Jwts.SIG.PS384): PS384_0_10_7, + (Jwts.SIG.PS512): PS512_0_10_7 ] - private static JcaX509CertificateConverter X509_CERT_CONVERTER = new JcaX509CertificateConverter() - private static JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter() - - private static PEMParser getParser(String filename) { - InputStream is = Issue542Test.class.getResourceAsStream(filename) - return new PEMParser(new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1))) - } - - private static PublicKey readTestPublicKey(SignatureAlgorithm alg) { - PEMParser parser = getParser(alg.name() + '.crt.pem') - X509CertificateHolder holder = parser.readObject() as X509CertificateHolder - try { - return X509_CERT_CONVERTER.getCertificate(holder).getPublicKey() - } finally { - parser.close() - } - } - - private static PrivateKey readTestPrivateKey(SignatureAlgorithm alg) { - PEMParser parser = getParser(alg.name() + '.key.pem') - PrivateKeyInfo info = parser.readObject() as PrivateKeyInfo - try { - return PEM_KEY_CONVERTER.getPrivateKey(info) - } finally { - parser.close() - } - } - /** * Asserts backwards-compatibility for https://github.com/jwtk/jjwt/issues/542 */ @Test void testRsaSsaPssBackwardsCompatibility() { - def algs = [SignatureAlgorithm.PS256, SignatureAlgorithm.PS384, SignatureAlgorithm.PS512] + def algs = [Jwts.SIG.PS256, Jwts.SIG.PS384, Jwts.SIG.PS512] for (alg in algs) { - PublicKey key = readTestPublicKey(alg) + PublicKey key = TestKeys.forAlgorithm(alg).pair.public String jws = JWS_0_10_7_VALUES[alg] def token = Jwts.parser().setSigningKey(key).parseClaimsJws(jws) - assert 'joe' == token.body.getIssuer() + assert 'joe' == token.payload.getIssuer() } } @@ -85,11 +66,11 @@ class Issue542Test { * class. This method implementation was retained only to demonstrate how they were created for future reference. */ static void main(String[] args) { - def algs = [SignatureAlgorithm.PS256, SignatureAlgorithm.PS384, SignatureAlgorithm.PS512] + def algs = [Jwts.SIG.PS256, Jwts.SIG.PS384, Jwts.SIG.PS512] for (alg in algs) { - PrivateKey privateKey = readTestPrivateKey(alg) + PrivateKey privateKey = TestKeys.forAlgorithm(alg).pair.private String jws = Jwts.builder().setIssuer('joe').signWith(privateKey, alg).compact() - println "private static String ${alg.name()}_0_10_7 = '$jws'" + println "private static String ${alg.getId()}_0_10_7 = '$jws'" } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy new file mode 100644 index 00000000..37bd3416 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.security.SecurityException +import io.jsonwebtoken.security.SignatureException +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.Test + +import javax.crypto.Cipher +import javax.crypto.Mac +import java.security.Provider +import java.security.Security +import java.security.Signature + +import static org.junit.Assert.* + +class JcaTemplateTest { + + static final Provider SUN_PROVIDER = Security.getProvider('SunJCE') + static final Provider BC_PROVIDER = new BouncyCastleProvider() + + @Test + void testGetInstanceExceptionMessage() { + def factories = JcaTemplate.FACTORIES + for(def factory : factories) { + def clazz = factory.getInstanceClass() + try { + factory.get('foo', null) + } catch (SecurityException expected) { + if (clazz == Signature || clazz == Mac) { + assertTrue expected instanceof SignatureException + } + String prefix = "Unable to obtain ${clazz.getSimpleName()} instance " + + "from default JCA Provider for JCA algorithm 'foo': " + assertTrue expected.getMessage().startsWith(prefix) + } + } + } + + @Test + void testGetInstanceWithExplicitProviderExceptionMessage() { + def factories = JcaTemplate.FACTORIES + def provider = BC_PROVIDER + for(def factory : factories) { + def clazz = factory.getInstanceClass() + try { + factory.get('foo', provider) + } catch (SecurityException expected) { + if (clazz == Signature || clazz == Mac) { + assertTrue expected instanceof SignatureException + } + String prefix = "Unable to obtain ${clazz.getSimpleName()} instance " + + "from specified Provider '${provider.toString()}' for JCA algorithm 'foo': " + assertTrue expected.getMessage().startsWith(prefix) + } + } + } + + @Test + void testCallbackThrowsSecurityException() { + // tests that any callback that throws a SecurityException doesn't need to be wrapped + String msg = 'fubar' + def template = new JcaTemplate('AES/CBC/PKCS5Padding', null) + try { + template.withCipher(new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + throw new SecurityException(msg) + } + }) + } catch (SecurityException ex) { + assertEquals msg, ex.getMessage() + } + } + + @Test + void testNewCipherWithExplicitProvider() { + Provider provider = SUN_PROVIDER + def template = new JcaTemplate('AES/CBC/PKCS5Padding', provider) + template.withCipher(new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + assertNotNull cipher + assertSame provider, cipher.getProvider() + return new byte[0] + } + }) + } + +// @Test +// void testGetInstanceFailureWithExplicitProvider() { +// //noinspection GroovyUnusedAssignment +// Provider provider = Security.getProvider('SunJCE') +// def supplier = new JcaTemplate.JcaInstanceSupplier(Cipher.class, "AES", provider) { +// @Override +// protected Cipher doGetInstance() { +// throw new IllegalStateException("foo") +// } +// } +// +// try { +// supplier.getInstance() +// } catch (SecurityException ce) { //should be wrapped as SecurityException +// String msg = ce.getMessage() +// //we check for starts-with/ends-with logic here instead of equals because the JCE provider String value +// //contains the JCE version number, and that can differ across JDK versions. Since we use different JDK +// //versions in the test machine matrix, we don't want test failures from JDKs that run on higher versions +// assertTrue msg.startsWith('Unable to obtain Cipher instance from specified Provider {SunJCE') +// assertTrue msg.endsWith('} for JCA algorithm \'AES\': foo') +// } +// } +// +// @Test +// void testGetInstanceDoesNotWrapCryptoExceptions() { +// def ex = new SecurityException("foo") +// def supplier = new JcaTemplate.JcaInstanceSupplier(Cipher.class, 'AES', null) { +// @Override +// protected Cipher doGetInstance() { +// throw ex +// } +// } +// +// try { +// supplier.getInstance() +// } catch (SecurityException ce) { +// assertSame ex, ce +// } +// } +// +// static void wrapInSignatureException(Class instanceType, String jcaName) { +// def ex = new IllegalArgumentException("foo") +// def supplier = new JcaTemplate.JcaInstanceSupplier(instanceType, jcaName, null) { +// @Override +// protected Object doGetInstance() { +// throw ex +// } +// } +// +// try { +// supplier.getInstance() +// } catch (SignatureException se) { +// assertSame ex, se.getCause() +// String msg = "Unable to obtain ${instanceType.simpleName} instance from default JCA Provider for JCA algorithm '${jcaName}': foo" +// assertEquals msg, se.getMessage() +// } +// } + +// @Test +// void testNonCryptoExceptionForSignatureOrMacInstanceIsWrappedInSignatureException() { +// wrapInSignatureException(Signature.class, 'RSA') +// wrapInSignatureException(Mac.class, 'HmacSHA256') +// } + + @Test + void testCallbackThrowsException() { + def ex = new Exception("testing") + def template = new JcaTemplate('AES/CBC/PKCS5Padding', null) + try { + template.withCipher(new CheckedFunction() { + @Override + byte[] apply(Cipher cipher) throws Exception { + throw ex + } + }) + } catch (SecurityException e) { + assertEquals 'Cipher callback execution failed: testing', e.getMessage() + assertSame ex, e.getCause() + } + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy new file mode 100644 index 00000000..b542111e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkConverterTest.groovy @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.security.* +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class JwkConverterTest { + + static String typeString(def target) { + return JwkConverter.typeString(target) + } + + @Test + void testJwkClassTypeString() { + assertEquals 'JWK', typeString(Jwk.class) + } + + @Test + void testSecretJwkClassTypeString() { + assertEquals 'Secret JWK', typeString(SecretJwk.class) + } + + @Test + void testSecretJwkTypeString() { + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + assertEquals 'Secret JWK', typeString(jwk) + } + + @Test + void testPublicJwkClassTypeString() { + assertEquals 'Public JWK', typeString(PublicJwk.class) + } + + @Test + void testEcPublicJwkClassTypeString() { + assertEquals 'EC Public JWK', typeString(EcPublicJwk.class) + } + + @Test + void testEdPublicJwkClassTypeString() { + assertEquals 'Edwards Curve Public JWK', typeString(OctetPublicJwk.class) + } + + @Test + void testRsaPublicJwkClassTypeString() { + assertEquals 'RSA Public JWK', typeString(RsaPublicJwk.class) + } + + @Test + void testPrivateJwkClassTypeString() { + assertEquals 'Private JWK', typeString(PrivateJwk.class) + } + + @Test + void testEcPrivateJwkClassTypeString() { + assertEquals 'EC Private JWK', typeString(EcPrivateJwk.class) + } + + @Test + void testEdPrivateJwkClassTypeString() { + assertEquals 'Edwards Curve Private JWK', typeString(OctetPrivateJwk.class) + } + + @Test + void testRsaPrivateJwkClassTypeString() { + assertEquals 'RSA Private JWK', typeString(RsaPrivateJwk.class) + } + + @Test + void testPrivateJwk() { + JwkConverter converter = new JwkConverter<>(PrivateJwk.class) + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + try { + converter.applyFrom(jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Value must be a Private JWK, not a Secret JWK." + assertEquals msg, expected.getMessage() + } + } + + @Test + void testRsaPrivateJwk() { + JwkConverter converter = new JwkConverter<>(RsaPublicJwk.class) + def jwk = Jwks.builder().forKey(TestKeys.HS256).build() + try { + converter.applyFrom(jwk) + fail() + } catch (IllegalArgumentException expected) { + String msg = "Value must be an RSA Public JWK, not a Secret JWK." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy new file mode 100644 index 00000000..e39bf7e5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.gson.io.GsonDeserializer +import io.jsonwebtoken.gson.io.GsonSerializer +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.jackson.io.JacksonDeserializer +import io.jsonwebtoken.jackson.io.JacksonSerializer +import io.jsonwebtoken.lang.Supplier +import io.jsonwebtoken.orgjson.io.OrgJsonDeserializer +import io.jsonwebtoken.orgjson.io.OrgJsonSerializer +import io.jsonwebtoken.security.Jwk +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.Key + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +/** + * Asserts that serializing and deserializing private or secret key values works as expected without + * exposing raw strings in the JWKs themselves (should be wrapped with RedactedSupplier instances) for toString safety. + */ +class JwkSerializationTest { + + @Test + void testJacksonSecretJwk() { + testSecretJwk(new JacksonSerializer(), new JacksonDeserializer()) + } + + @Test + void testJacksonPrivateEcJwk() { + testPrivateEcJwk(new JacksonSerializer(), new JacksonDeserializer()) + } + + @Test + void testJacksonPrivateRsaJwk() { + testPrivateRsaJwk(new JacksonSerializer(), new JacksonDeserializer()) + } + + @Test + void testGsonSecretJwk() { + testSecretJwk(new GsonSerializer(), new GsonDeserializer()) + } + + @Test + void testGsonPrivateEcJwk() { + testPrivateEcJwk(new GsonSerializer(), new GsonDeserializer()) + } + + @Test + void testGsonPrivateRsaJwk() { + testPrivateRsaJwk(new GsonSerializer(), new GsonDeserializer()) + } + + @Test + void testOrgJsonSecretJwk() { + testSecretJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) + } + + @Test + void testOrgJsonPrivateEcJwk() { + testPrivateEcJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) + } + + @Test + void testOrgJsonPrivateRsaJwk() { + testPrivateRsaJwk(new OrgJsonSerializer(), new OrgJsonDeserializer()) + } + + static void testSecretJwk(Serializer serializer, Deserializer deserializer) { + + def key = TestKeys.A128GCM + def jwk = Jwks.builder().forKey(key).setId('id').build() + assertWrapped(jwk, ['k']) + + // Ensure no Groovy or Java toString prints out secret values: + assertEquals '[kid:id, kty:oct, k:]', "$jwk" as String // groovy gstring + assertEquals '{kid=id, kty=oct, k=}', jwk.toString() // java toString + + //but serialization prints the real value: + byte[] data = serializer.serialize(jwk) + def result = new String(data, StandardCharsets.UTF_8) + // assert substrings here because JSON order is not guaranteed: + assertTrue result.contains('"kid":"id"') + assertTrue result.contains('"kty":"oct"') + assertTrue result.contains("\"k\":\"${jwk.k.get()}\"" as String) + + //now ensure it deserializes back to a JWK: + def map = deserializer.deserialize(data) as Map + def jwk2 = Jwks.builder().putAll(map).build() + assertTrue jwk.k instanceof Supplier + assertEquals jwk, jwk2 + assertEquals jwk.k, jwk2.k + assertEquals jwk.k.get(), jwk2.k.get() + } + + static void testPrivateEcJwk(Serializer serializer, Deserializer deserializer) { + + def jwk = Jwks.builder().forEcKeyPair(TestKeys.ES256.pair).setId('id').build() + assertWrapped(jwk, ['d']) + + // Ensure no Groovy or Java toString prints out secret values: + assertEquals '[kid:id, kty:EC, crv:P-256, x:xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y:_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, d:]', "$jwk" as String + // groovy gstring + assertEquals '{kid=id, kty=EC, crv=P-256, x=xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q, y=_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk, d=}', jwk.toString() + // java toString + + //but serialization prints the real value: + byte[] data = serializer.serialize(jwk) + def result = new String(data, StandardCharsets.UTF_8) + // assert substrings here because JSON order is not guaranteed: + assertTrue result.contains('"kid":"id"') + assertTrue result.contains('"kty":"EC"') + assertTrue result.contains('"crv":"P-256"') + assertTrue result.contains('"x":"xNKMMIsawShLG4LYxpNP0gqdgK_K69UXCLt3AE3zp-Q"') + assertTrue result.contains('"y":"_vzQymVtA7RHRTfBWZo75mxPgDkE8g7bdHI3siSuJOk"') + assertTrue result.contains("\"d\":\"${jwk.d.get()}\"" as String) + + //now ensure it deserializes back to a JWK: + def map = deserializer.deserialize(data) as Map + def jwk2 = Jwks.builder().putAll(map).build() + assertTrue jwk.d instanceof Supplier + assertEquals jwk, jwk2 + assertEquals jwk.d, jwk2.d + assertEquals jwk.d.get(), jwk2.d.get() + } + + private static assertWrapped(Map map, List keys) { + for (String key : keys) { + def value = map.get(key) + assertTrue value instanceof Supplier + value = ((Supplier) value).get() + assertTrue value instanceof String + } + } + + private static assertEquals(Jwk jwk1, Jwk jwk2, List keys) { + assertEquals jwk1, jwk2 + for (String key : keys) { + assertTrue jwk1.get(key) instanceof Supplier + assertTrue jwk2.get(key) instanceof Supplier + assertEquals jwk1.get(key), jwk2.get(key) + assertEquals jwk1.get(key).get(), jwk2.get(key).get() + } + } + + static void testPrivateRsaJwk(Serializer serializer, Deserializer deserializer) { + + def jwk = Jwks.builder().forRsaKeyPair(TestKeys.RS256.pair).setId('id').build() + def privateFieldNames = ['d', 'p', 'q', 'dp', 'dq', 'qi'] + assertWrapped(jwk, privateFieldNames) + + // Ensure no Groovy or Java toString prints out secret values: + assertEquals '[kid:id, kty:RSA, n:zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ_XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6NdFxh6T_pMXpkf8d81fzVo4ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, e:AQAB, d:, p:, q:, dp:, dq:, qi:]', "$jwk" as String + // groovy gstring + assertEquals '{kid=id, kty=RSA, n=zkH0MwxQ2cUFWsvOPVFqI_dk2EFTjQolCy97mI5_wYCbaOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ_XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS-MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4-5-7MkC33-8-neZUzS7b6NdFxh6T_pMXpkf8d81fzVo4ZBMloweW0_l8MOdVxeX7M_7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8Q, e=AQAB, d=, p=, q=, dp=, dq=, qi=}', jwk.toString() + // java toString + + //but serialization prints the real value: + byte[] data = serializer.serialize(jwk) + def result = new String(data, StandardCharsets.UTF_8) + // assert substrings here because JSON order is not guaranteed: + assertTrue result.contains('"kid":"id"') + assertTrue result.contains('"kty":"RSA"') + assertTrue result.contains('"e":"AQAB"') + assertTrue result.contains("\"n\":\"${jwk.n}\"" as String) //public property, not wrapped + assertTrue result.contains("\"d\":\"${jwk.d.get()}\"" as String) // all remaining should be wrapped + assertTrue result.contains("\"p\":\"${jwk.p.get()}\"" as String) + assertTrue result.contains("\"q\":\"${jwk.q.get()}\"" as String) + assertTrue result.contains("\"dp\":\"${jwk.dp.get()}\"" as String) + assertTrue result.contains("\"dq\":\"${jwk.dq.get()}\"" as String) + assertTrue result.contains("\"qi\":\"${jwk.qi.get()}\"" as String) + + //now ensure it deserializes back to a JWK: + def map = deserializer.deserialize(data) as Map + def jwk2 = Jwks.builder().putAll(map).build() + assertEquals(jwk, jwk2, privateFieldNames) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkThumbprintsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkThumbprintsTest.groovy new file mode 100644 index 00000000..82c23886 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkThumbprintsTest.groovy @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.RfcTests +import io.jsonwebtoken.security.HashAlgorithm +import io.jsonwebtoken.security.JwkThumbprint +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.StandardHashAlgorithms +import org.junit.Test + +import javax.crypto.SecretKey +import java.nio.charset.StandardCharsets + +import static io.jsonwebtoken.impl.security.DefaultHashAlgorithm.SHA1 +import static org.junit.Assert.assertEquals + +class JwkThumbprintsTest { + + static final HashAlgorithm SHA256 = StandardHashAlgorithms.get().SHA256; + + static byte[] digest(String json, HashAlgorithm alg) { + def utf8Bytes = json.getBytes(StandardCharsets.UTF_8) + def req = new DefaultRequest(utf8Bytes, null, null) + return alg.digest(req) + } + + static JwkThumbprint thumbprint(String json, HashAlgorithm alg) { + return new DefaultJwkThumbprint(digest(json, alg), alg) + } + + @Test + void testSecretJwks() { + TestKeys.SECRET.each { SecretKey key -> + def jwk = Jwks.builder().forKey((SecretKey) key).setIdFromThumbprint().build() + def json = RfcTests.stripws(""" + {"k":"${jwk.get('k').get()}","kty":"oct"} + """) + def s256t = thumbprint(json, SHA256) + assertEquals s256t, jwk.thumbprint() + assertEquals thumbprint(json, SHA1), jwk.thumbprint(SHA1) + assertEquals s256t.toString(), jwk.getId() + } + } + + @Test + void testRsaKeyPair() { + def pair = TestKeys.RS256.pair + def privJwk = Jwks.builder().forRsaKeyPair(pair).setIdFromThumbprint().build() + def pubJwk = privJwk.toPublicJwk() + def json = RfcTests.stripws(""" + {"e":"${pubJwk.get('e')}","kty":"RSA","n":"${pubJwk.get('n')}"} + """) + + def s256t = thumbprint(json, SHA256) + + assertEquals s256t, pubJwk.thumbprint() + assertEquals thumbprint(json, SHA1), pubJwk.thumbprint(SHA1) + assertEquals s256t.toString(), pubJwk.getId() + + assertEquals thumbprint(json, SHA256), privJwk.thumbprint() + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + assertEquals thumbprint(json, SHA1), privJwk.thumbprint(SHA1) + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + assertEquals s256t.toString(), privJwk.getId() + } + + @Test + void testEcKeyPair() { + def pair = TestKeys.ES256.pair + def privJwk = Jwks.builder().forEcKeyPair(pair).setIdFromThumbprint().build() + def pubJwk = privJwk.toPublicJwk() + def json = RfcTests.stripws(""" + {"crv":"${pubJwk.get('crv')}","kty":"EC","x":"${pubJwk.get('x')}","y":"${pubJwk.get('y')}"} + """) + + def s256t = thumbprint(json, SHA256) + + assertEquals s256t, pubJwk.thumbprint() + assertEquals thumbprint(json, SHA1), pubJwk.thumbprint(SHA1) + assertEquals s256t.toString(), pubJwk.getId() + + assertEquals thumbprint(json, SHA256), privJwk.thumbprint() + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + assertEquals thumbprint(json, SHA1), privJwk.thumbprint(SHA1) + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + assertEquals s256t.toString(), privJwk.getId() + } + + @Test + void testEdECKeyPair() { + def pair = TestKeys.Ed25519.pair + def privJwk = Jwks.builder().forOctetKeyPair(pair).setIdFromThumbprint().build() + def pubJwk = privJwk.toPublicJwk() + def json = RfcTests.stripws(""" + {"crv":"${pubJwk.get('crv')}","kty":"OKP","x":"${pubJwk.get('x')}"} + """) + + def s256t = thumbprint(json, SHA256) + + assertEquals s256t, pubJwk.thumbprint() + assertEquals thumbprint(json, SHA1), pubJwk.thumbprint(SHA1) + assertEquals s256t.toString(), pubJwk.getId() + + assertEquals thumbprint(json, SHA256), privJwk.thumbprint() + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + assertEquals thumbprint(json, SHA1), privJwk.thumbprint(SHA1) + // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 + assertEquals s256t.toString(), privJwk.getId() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy new file mode 100644 index 00000000..116c772b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Converters +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.security.interfaces.ECKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint + +import static org.junit.Assert.* + +class JwksTest { + + private static final SecretKey SKEY = Jwts.SIG.HS256.keyBuilder().build() + private static final java.security.KeyPair EC_PAIR = Jwts.SIG.ES256.keyPairBuilder().build() + + private static String srandom() { + byte[] random = new byte[16] + Randoms.secureRandom().nextBytes(random) + return Encoders.BASE64URL.encode(random) + } + + static void testProperty(String name, String id, def val, def expectedFieldValue = val) { + String cap = "${name.capitalize()}" + def key = name == 'publicKeyUse' || name == 'x509CertificateChain' ? EC_PAIR.public : SKEY + + //test non-null value: + //noinspection GroovyAssignabilityCheck + def builder = Jwks.builder().forKey(key) + builder."set${cap}"(val) + def jwk = builder.build() + assertEquals val, jwk."get${cap}"() + assertEquals expectedFieldValue, jwk."${id}" + + //test null value: + builder = Jwks.builder().forKey(key) + try { + builder."set${cap}"(null) + fail("IAE should have been thrown") + } catch (IllegalArgumentException ignored) { + } + jwk = builder.build() + assertNull jwk."get${cap}"() + assertNull jwk."$id" + assertFalse jwk.containsKey(id) + + //test empty string value + builder = Jwks.builder().forKey(key) + if (val instanceof String) { + try { + builder."set${cap}"(' ' as String) + fail("IAE should have been thrown") + } catch (IllegalArgumentException ignored) { + } + jwk = builder.build() + assertNull jwk."get${cap}"() + assertNull jwk."$id" + assertFalse jwk.containsKey(id) + } + + //test empty value + if (val instanceof List) { + val = Collections.emptyList() + } else if (val instanceof Set) { + val = Collections.emptySet() + } + if (val instanceof Collection) { + try { + builder."set${cap}"(val) + fail("IAE should have been thrown") + } catch (IllegalArgumentException ignored) { + } + jwk = builder.build() + assertNull jwk."get${cap}"() + assertNull jwk."$id" + assertFalse jwk.containsKey(id) + } + } + + @Test + void testPrivateCtor() { + new Jwks() // for code coverage only + } + + @Test + void testBuilderWithoutState() { + try { + Jwks.builder().build() + fail() + } catch (IllegalStateException ise) { + String msg = 'A java.security.Key or one or more name/value pairs must be provided to create a JWK.' + assertEquals msg, ise.getMessage() + } + } + + @Test + void testBuilderWithSecretKey() { + def jwk = Jwks.builder().forKey(SKEY).build() + assertEquals 'oct', jwk.getType() + assertEquals 'oct', jwk.kty + String k = jwk.k.get() as String + assertNotNull k + assertTrue MessageDigest.isEqual(SKEY.encoded, Decoders.BASE64URL.decode(k)) + } + + @Test + void testAlgorithm() { + testProperty('algorithm', 'alg', srandom()) + } + + @Test + void testId() { + testProperty('id', 'kid', srandom()) + } + + @Test + void testOperations() { + testProperty('operations', 'key_ops', ['foo', 'bar'] as Set) + } + + @Test + void testPublicKeyUse() { + testProperty('publicKeyUse', 'use', srandom()) + } + + @Test + void testX509CertChain() { + //get a test cert: + X509Certificate cert = TestKeys.forAlgorithm(Jwts.SIG.RS256).cert + def sval = JwtX509StringConverter.INSTANCE.applyTo(cert) + testProperty('x509CertificateChain', 'x5c', [cert], [sval]) + } + + @Test + void testX509Sha1Thumbprint() { + testX509Thumbprint(1) + } + + @Test + void testX509Sha256Thumbprint() { + testX509Thumbprint(256) + } + + @Test + void testRandom() { + def random = new SecureRandom() + def jwk = Jwks.builder().forKey(SKEY).setRandom(random).build() + assertSame random, jwk.@context.getRandom() + } + + @Test + void testNullRandom() { + assertNotNull Jwks.builder().forKey(SKEY).setRandom(null).build() + } + + static void testX509Thumbprint(int number) { + def algs = Jwts.SIG.values().findAll { it instanceof SignatureAlgorithm } + + for (def alg : algs) { + //get test cert: + X509Certificate cert = TestCertificates.readTestCertificate(alg) + def pubKey = cert.getPublicKey() + + def builder = Jwks.builder() + if (pubKey instanceof ECKey) { + builder = builder.forEcChain(cert) + } else if (pubKey instanceof RSAKey) { + builder = builder.forRsaChain(cert) + } else { + builder = builder.forOctetChain(cert) + } + + if (number == 1) { + builder.withX509Sha1Thumbprint(true) + } // otherwise, when a chain is present, a sha256 thumbprint is calculated automatically + + def jwkFromKey = builder.build() as PublicJwk + byte[] thumbprint = jwkFromKey."getX509CertificateSha${number}Thumbprint"() + assertNotNull thumbprint + + //ensure base64url encoding/decoding of the thumbprint works: + def jwkFromValues = Jwks.builder().putAll(jwkFromKey).build() as PublicJwk + assertArrayEquals thumbprint, jwkFromValues."getX509CertificateSha${number}Thumbprint"() as byte[] + } + } + + @Test + void testSecretJwks() { + Collection algs = Jwts.SIG.values().findAll({ it instanceof MacAlgorithm }) as Collection + for (def alg : algs) { + SecretKey secretKey = alg.keyBuilder().build() + def jwk = Jwks.builder().forKey(secretKey).setId('id').build() + assertEquals 'oct', jwk.getType() + assertTrue jwk.containsKey('k') + assertEquals 'id', jwk.getId() + assertEquals secretKey, jwk.toKey() + } + } + + @Test + void testSecretKeyGetEncodedReturnsNull() { + SecretKey key = new TestSecretKey(algorithm: "AES") + try { + Jwks.builder().forKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String expectedMsg = 'Unable to encode SecretKey to JWK: ' + SecretJwkFactory.ENCODED_UNAVAILABLE_MSG + assertEquals expectedMsg, expected.getMessage() + assertTrue expected.getCause() instanceof IllegalArgumentException + assertEquals SecretJwkFactory.ENCODED_UNAVAILABLE_MSG, expected.getCause().getMessage() + } + } + + @Test + void testSecretKeyGetEncodedThrowsException() { + String encodedMsg = "not allowed" + def encodedEx = new UnsupportedOperationException(encodedMsg) + SecretKey key = new TestSecretKey() { + @Override + byte[] getEncoded() { + throw encodedEx + } + } + try { + Jwks.builder().forKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String expectedMsg = 'Unable to encode SecretKey to JWK: ' + SecretJwkFactory.ENCODED_UNAVAILABLE_MSG + assertEquals expectedMsg, expected.getMessage() + assertTrue expected.getCause() instanceof IllegalArgumentException + assertEquals SecretJwkFactory.ENCODED_UNAVAILABLE_MSG, expected.getCause().getMessage() + assertSame encodedEx, expected.getCause().getCause() + } + } + + @Test + void testAsymmetricJwks() { + + Collection algs = Jwts.SIG.values().findAll({ it instanceof SignatureAlgorithm }) as Collection + + for (def alg : algs) { + + def pair = alg.keyPairBuilder().build() + PublicKey pub = pair.getPublic() + PrivateKey priv = pair.getPrivate() + + // test individual keys + PublicJwk pubJwk = Jwks.builder().forKey(pub).setPublicKeyUse("sig").build() + assertEquals pub, pubJwk.toKey() + + def builder = Jwks.builder().forKey(priv).setPublicKeyUse('sig') + if (alg instanceof EdSignatureAlgorithm) { + // We haven't implemented EdDSA public-key derivation yet, so public key is required + builder.setPublicKey(pub) + } + PrivateJwk privJwk = builder.build() + assertEquals priv, privJwk.toKey() + PublicJwk privPubJwk = privJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pub, pubJwk.toKey() + def jwkPair = privJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + assertEquals priv, jwkPair.getPrivate() + + // test pair + if (pub instanceof ECKey) { + builder = Jwks.builder().forEcKeyPair(pair) + } else if (pub instanceof RSAKey) { + builder = Jwks.builder().forRsaKeyPair(pair) + } else { + builder = Jwks.builder().forOctetKeyPair(pair) + } + privJwk = builder.setPublicKeyUse("sig").build() as PrivateJwk + assertEquals priv, privJwk.toKey() + privPubJwk = privJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pub, pubJwk.toKey() + jwkPair = privJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + assertEquals priv, jwkPair.getPrivate() + } + } + + @Test + void testInvalidEcCurvePoint() { + def algs = [Jwts.SIG.ES256, Jwts.SIG.ES384, Jwts.SIG.ES512] + + for (SignatureAlgorithm alg : algs) { + + def pair = alg.keyPairBuilder().build() + ECPublicKey pubKey = pair.getPublic() as ECPublicKey + + EcPublicJwk jwk = Jwks.builder().forKey(pubKey).build() + + //try creating a JWK with a bad point: + def badPubKey = new InvalidECPublicKey(pubKey) + try { + Jwks.builder().forKey(badPubKey).build() + } catch (InvalidKeyException ike) { + String curveId = jwk.get('crv') + String msg = EcPublicJwkFactory.keyContainsErrorMessage(curveId) + assertEquals msg, ike.getMessage() + } + + BigInteger p = pubKey.getParams().getCurve().getField().getP() + def outOfFieldRange = [BigInteger.ZERO, BigInteger.ONE, p, p.add(BigInteger.valueOf(1))] + for (def x : outOfFieldRange) { + Map modified = new LinkedHashMap<>(jwk) + modified.put('x', Converters.BIGINT.applyTo(x)) + try { + Jwks.builder().putAll(modified).build() + } catch (InvalidKeyException ike) { + String expected = EcPublicJwkFactory.jwkContainsErrorMessage(jwk.crv as String, modified) + assertEquals(expected, ike.getMessage()) + } + } + for (def y : outOfFieldRange) { + Map modified = new LinkedHashMap<>(jwk) + modified.put('y', Converters.BIGINT.applyTo(y)) + try { + Jwks.builder().putAll(modified).build() + } catch (InvalidKeyException ike) { + String expected = EcPublicJwkFactory.jwkContainsErrorMessage(jwk.crv as String, modified) + assertEquals(expected, ike.getMessage()) + } + } + } + } + + @Test + void testPublicJwkBuilderWithRSAPublicKey() { + def key = TestKeys.RS256.pair.public + // must cast to PublicKey to avoid Groovy's dynamic type dispatch to the forKey(RSAPublicKey) method: + def jwk = Jwks.builder().forKey((PublicKey) key).build() + assertNotNull jwk + assertTrue jwk instanceof RsaPublicJwk + } + + @Test + void testPublicJwkBuilderWithECPublicKey() { + def key = TestKeys.ES256.pair.public + // must cast to PublicKey to avoid Groovy's dynamic type dispatch to the forKey(ECPublicKey) method: + def jwk = Jwks.builder().forKey((PublicKey) key).build() + assertNotNull jwk + assertTrue jwk instanceof EcPublicJwk + } + + @Test + void testPublicJwkBuilderWithUnsupportedKey() { + def key = new TestPublicKey() + // must cast to PublicKey to avoid Groovy's dynamic type dispatch to the forKey(ECPublicKey) method: + try { + Jwks.builder().forKey((PublicKey) key) + } catch (UnsupportedKeyException expected) { + String msg = 'There is no builder that supports specified key of type io.jsonwebtoken.impl.security.TestPublicKey with algorithm \'null\'.' + assertEquals(msg, expected.getMessage()) + assertNotNull expected.getCause() // ensure we always retain a cause + } + } + + @Test + void testPrivateJwkBuilderWithRSAPrivateKey() { + def key = TestKeys.RS256.pair.private + // must cast to PrivateKey to avoid Groovy's dynamic type dispatch to the forKey(RSAPrivateKey) method: + def jwk = Jwks.builder().forKey((PrivateKey) key).build() + assertNotNull jwk + assertTrue jwk instanceof RsaPrivateJwk + } + + @Test + void testPrivateJwkBuilderWithECPrivateKey() { + def key = TestKeys.ES256.pair.private + // must cast to PrivateKey to avoid Groovy's dynamic type dispatch to the forKey(ECPrivateKey) method: + def jwk = Jwks.builder().forKey((PrivateKey) key).build() + assertNotNull jwk + assertTrue jwk instanceof EcPrivateJwk + } + + @Test + void testPrivateJwkBuilderWithUnsupportedKey() { + def key = new TestPrivateKey() + try { + Jwks.builder().forKey((PrivateKey) key) + } catch (UnsupportedKeyException expected) { + String msg = 'There is no builder that supports specified key of type io.jsonwebtoken.impl.security.TestPrivateKey with algorithm \'null\'.' + assertEquals(msg, expected.getMessage()) + assertNotNull expected.getCause() // ensure we always retain a cause + } + } + + private static class InvalidECPublicKey implements ECPublicKey { + + private final ECPublicKey good + + InvalidECPublicKey(ECPublicKey good) { + this.good = good + } + + @Override + ECPoint getW() { + return ECPoint.POINT_INFINITY // bad value, should make all 'contains' validations fail + } + + @Override + String getAlgorithm() { + return good.getAlgorithm() + } + + @Override + String getFormat() { + return good.getFormat() + } + + @Override + byte[] getEncoded() { + return good.getEncoded() + } + + @Override + ECParameterSpec getParams() { + return good.getParams() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy new file mode 100644 index 00000000..db17aca3 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwtX509StringConverterTest.groovy @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.lang.Bytes +import org.junit.Test + +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* + +class JwtX509StringConverterTest { + + @Test + void testApplyToThrowsEncodingException() { + + def ex = new CertificateEncodingException("foo") + + X509Certificate cert = createMock(X509Certificate) + expect(cert.getEncoded()).andThrow(ex) + replay cert + + try { + JwtX509StringConverter.INSTANCE.applyTo(cert) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = 'Unable to access X509Certificate encoded bytes necessary to perform DER ' + + 'Base64-encoding. Certificate: {EasyMock for class java.security.cert.X509Certificate}. ' + + 'Cause: ' + ex.getMessage() + assertSame ex, expected.getCause() + assertEquals expectedMsg, expected.getMessage() + } + + verify cert + } + + @Test + void testApplyToWithEmptyEncoding() { + + X509Certificate cert = createMock(X509Certificate) + expect(cert.getEncoded()).andReturn(Bytes.EMPTY) + replay cert + + try { + JwtX509StringConverter.INSTANCE.applyTo(cert) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = 'X509Certificate encoded bytes cannot be null or empty. Certificate: ' + + '{EasyMock for class java.security.cert.X509Certificate}.' + assertEquals expectedMsg, expected.getMessage() + } + + verify cert + } + + @Test + void testApplyFromThrowsCertificateException() { + + def converter = new JwtX509StringConverter() { + @Override + protected CertificateFactory newCertificateFactory() throws CertificateException { + throw new CertificateException("nope") + } + } + + String s = 'foo' + try { + converter.applyFrom(s) + fail() + } catch (IllegalArgumentException expected) { + String expectedMsg = "Unable to convert Base64 String '$s' to X509Certificate instance. Cause: nope" + assertEquals expectedMsg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy new file mode 100644 index 00000000..1975e85d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyPairsTest.groovy @@ -0,0 +1,127 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import org.junit.Test + +import java.security.Key +import java.security.KeyPair +import java.security.PublicKey +import java.security.interfaces.DSAPublicKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class KeyPairsTest { + + @Test + void testPrivateCtor() { // for code coverage only + new KeyPairs() + } + + @Test + void testGetKeyNullPair() { + try { + KeyPairs.getKey(null, ECPublicKey.class) + fail() + } catch (IllegalArgumentException iae) { + assertEquals 'KeyPair cannot be null.', iae.getMessage() + } + } + + @Test + void testUnrecognizedFamily() { + PublicKey pub = new TestECPublicKey() + KeyPair pair = new KeyPair(pub, new TestECPrivateKey()) + Class clazz = DSAPublicKey // unrecognized --> no 'family' prefix in message + try { + KeyPairs.getKey(pair, clazz) + fail() + } catch (IllegalArgumentException iae) { + String msg = "KeyPair public key must be an instance of ${clazz.name}. Type found: ${pub.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testGetKeyECMismatch() { + KeyPair pair = Jwts.SIG.RS256.keyPairBuilder().build() + Class clazz = ECPublicKey + try { + KeyPairs.getKey(pair, clazz) + } catch (IllegalArgumentException iae) { + String msg = "EC KeyPair public key must be an instance of ${clazz.name}. Type found: ${pair.public.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testGetKeyRSAMismatch() { + KeyPair pair = new KeyPair(new TestECPublicKey(), new TestECPrivateKey()) + Class clazz = RSAPublicKey + try { + KeyPairs.getKey(pair, clazz) + } catch (IllegalArgumentException iae) { + String msg = "RSA KeyPair public key must be an instance of ${clazz.name}. Type found: ${pair.public.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testAssertPublicKeyTypeMismatch() { + Key key = new TestECPublicKey() + Class clazz = RSAPublicKey + String prefix = 'Foo ' + try { + KeyPairs.assertKey(key, clazz, prefix) + fail() + } catch (IllegalArgumentException iae) { + String msg = "${prefix}public key must be an instance of ${clazz.name}. Type found: ${key.class.name}" + assertEquals msg, iae.getMessage() + } + } + + @Test + void testAssertPrivateKeyTypeMismatch() { + Key key = new TestECPrivateKey() + Class clazz = RSAPrivateKey + String prefix = 'Foo ' + try { + KeyPairs.assertKey(key, clazz, prefix) + fail() + } catch (IllegalArgumentException iae) { + String msg = "${prefix}private key must be an instance of ${clazz.name}. Type found: ${key.class.name}" + assertEquals msg, iae.getMessage() + } + } + + private void printMap(Map m, int indentCount) { + for (def entry : m.entrySet()) { + indentCount.times { print("\t") } + print "${entry.key}: " + if (entry.value instanceof Map) { + println() + printMap(entry.value as Map, indentCount + 1) + } else { + println "${entry.value}" + } + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy new file mode 100644 index 00000000..f3e435f3 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeyUsageTest.groovy @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 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.security + +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +class KeyUsageTest { + + private static KeyUsage usage(int trueIndex) { + boolean[] usage = new boolean[9] + usage[trueIndex] = true + return new KeyUsage(new TestX509Certificate(keyUsage: usage)) + } + + private KeyUsage ku + + @Before + void setUp() { + ku = new KeyUsage(new TestX509Certificate()) + } + + @Test + void testNullCert() { + ku = new KeyUsage(null) + assertFalse ku.isCRLSign() + assertFalse ku.isDataEncipherment() + assertFalse ku.isDecipherOnly() + assertFalse ku.isDigitalSignature() + assertFalse ku.isEncipherOnly() + assertFalse ku.isKeyAgreement() + assertFalse ku.isKeyCertSign() + assertFalse ku.isKeyEncipherment() + assertFalse ku.isNonRepudiation() + } + + @Test + void testCertWithNullKeyUsage() { + ku = new KeyUsage(new TestX509Certificate(keyUsage: null)) + assertFalse ku.isCRLSign() + assertFalse ku.isDataEncipherment() + assertFalse ku.isDecipherOnly() + assertFalse ku.isDigitalSignature() + assertFalse ku.isEncipherOnly() + assertFalse ku.isKeyAgreement() + assertFalse ku.isKeyCertSign() + assertFalse ku.isKeyEncipherment() + assertFalse ku.isNonRepudiation() + } + + @Test + void testDigitalSignature() { + assertFalse ku.isDigitalSignature() //default + assertTrue usage(0).isDigitalSignature() + } + + @Test + void testNonRepudiation() { + assertFalse ku.isNonRepudiation() + assertTrue usage(1).isNonRepudiation() + } + + @Test + void testKeyEncipherment() { + assertFalse ku.isKeyEncipherment() + assertTrue usage(2).isKeyEncipherment() + } + + @Test + void testDataEncipherment() { + assertFalse ku.isDataEncipherment() + assertTrue usage(3).isDataEncipherment() + } + + @Test + void testKeyAgreement() { + assertFalse ku.isKeyAgreement() + assertTrue usage(4).isKeyAgreement() + } + + @Test + void testKeyCertSign() { + assertFalse ku.isKeyCertSign() + assertTrue usage(5).isKeyCertSign() + } + + @Test + void testCRLSign() { + assertFalse ku.isCRLSign() + assertTrue usage(6).isCRLSign() + } + + @Test + void testEncipherOnly() { + assertFalse ku.isEncipherOnly() + assertTrue usage(7).isEncipherOnly() + } + + @Test + void testDecipherOnly() { + assertFalse ku.isDecipherOnly() + assertTrue usage(8).isDecipherOnly() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy new file mode 100644 index 00000000..1beeb344 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy @@ -0,0 +1,60 @@ +/* + * 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.security + + +import org.junit.Test + +import java.security.Key + +import static org.junit.Assert.assertEquals + +class KeysBridgeTest { + + @Test + void testToStringKeyNull() { + assertEquals 'null', KeysBridge.toString(null) + } + + @Test + void testToStringPublicKey() { + // should just be key.toString(). Because it's a PublicKey, no danger of reporting key data + def key = TestKeys.ES256.pair.public + String s = KeysBridge.toString(key) + assertEquals key.toString(), s + } + + static void testFormattedOutput(Key key) { + String s = KeysBridge.toString(key) + String expected = "class: ${key.getClass().getName()}, algorithm: ${key.getAlgorithm()}, format: ${key.getFormat()}" as String + assertEquals expected, s + } + + @Test + void testToStringPrivateKey() { + testFormattedOutput(TestKeys.ES256.pair.private) + } + + @Test + void testToStringSecretKey() { + testFormattedOutput(TestKeys.HS256) + } + + @Test + void testToStringPassword() { + testFormattedOutput(new PasswordSpec("foo".toCharArray())) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy new file mode 100644 index 00000000..62f02d64 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/LocatingKeyResolverTest.groovy @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.DefaultClaims +import io.jsonwebtoken.impl.DefaultJwsHeader +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertSame + +class LocatingKeyResolverTest { + + @Test(expected = IllegalArgumentException) + void testNullConstructor() { + new LocatingKeyResolver(null) + } + + @Test + void testResolveSigningKeyClaims() { + def key = TestKeys.HS256 + def locator = new ConstantKeyLocator(key, null) + def header = new DefaultJwsHeader() + def claims = new DefaultClaims() + assertSame key, new LocatingKeyResolver(locator).resolveSigningKey(header, claims) + } + + @Test + void testResolveSigningKeyPayload() { + def key = TestKeys.HS256 + def locator = new ConstantKeyLocator(key, null) + def header = new DefaultJwsHeader() + def payload = 'hello world'.getBytes(StandardCharsets.UTF_8) + assertSame key, new LocatingKeyResolver(locator).resolveSigningKey(header, payload) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy new file mode 100644 index 00000000..66ab6fd6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/NoneSignatureAlgorithmTest.groovy @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.security.SecureRequest +import io.jsonwebtoken.security.SignatureException +import io.jsonwebtoken.security.VerifySecureDigestRequest +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +class NoneSignatureAlgorithmTest { + + private NoneSignatureAlgorithm alg + + @Before + void setUp() { + this.alg = new NoneSignatureAlgorithm() + } + + @Test + void testName() { + assertEquals "none", alg.getId() + } + + @Test(expected = SignatureException) + void testDigest() { + alg.digest((SecureRequest)null) + } + + @Test(expected = SignatureException) + void testVerify() { + alg.verify((VerifySecureDigestRequest)null) + } + + @Test + void testHashCode() { + assertEquals 'none'.hashCode(), alg.hashCode() + } + + @Test + void testEquals() { + assertTrue alg == new NoneSignatureAlgorithm() + } + + @Test + void testIdentityEquals() { + assertTrue alg == alg + } + + @Test + void testToString() { + assertEquals alg.getId(), alg.toString() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/OctetJwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/OctetJwksTest.groovy new file mode 100644 index 00000000..90a07518 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/OctetJwksTest.groovy @@ -0,0 +1,227 @@ +/* + * 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.security + +import io.jsonwebtoken.impl.RfcTests +import io.jsonwebtoken.security.* +import org.junit.Test + +import java.security.PrivateKey +import java.security.PublicKey + +import static org.junit.Assert.* + +class OctetJwksTest { + + /** + * Test case discovered during CI testing where a randomly-generated X25519 public key with a leading zero byte + * was not being decoded correctly. This test asserts that this value is decoded correctly. + */ + @Test + void testX25519PublicJson() { + String use = 'sig' + String kty = 'OKP' + String crv = 'X25519' + String x = 'AHwi7xPo5meUAGBDyzLZ9_ZwmmYA_SAMpdRFnsmggnI' + byte[] decoded = DefaultOctetPublicJwk.X.applyFrom(x) + assertEquals 0x00, decoded[0] + + String json = RfcTests.stripws(""" + { + "use": "$use", + "kty": "$kty", + "crv": "$crv", + "x": "$x" + }""") + def jwk = Jwks.parser().build().parse(json) as OctetPublicJwk + assertEquals use, jwk.getPublicKeyUse() + assertEquals kty, jwk.getType() + assertEquals crv, jwk.get('crv') + assertEquals x, jwk.get('x') + } + + /** + * Test case discovered during CI testing where a randomly-generated Ed448 public key with a leading zero byte was + * not being decoded correctly. This test asserts that this value is decoded correctly. + */ + @Test + void testEd448PublicJson() { + String use = 'sig' + String kty = 'OKP' + String crv = 'Ed448' + String x = 'AKxj_Iz2y6IHq5KipsOYZJyUjClO1IbT396KQK15DFryNwowKKBvswQLWytxXHgqGkpG5PUWkuQA' + byte[] decoded = DefaultOctetPublicJwk.X.applyFrom(x) + assertEquals 0x00, decoded[0] + + String json = RfcTests.stripws(""" + { + "use": "$use", + "kty": "$kty", + "crv": "$crv", + "x": "$x" + }""") + def jwk = Jwks.parser().build().parse(json) as OctetPublicJwk + assertEquals use, jwk.getPublicKeyUse() + assertEquals kty, jwk.getType() + assertEquals crv, jwk.get('crv') + assertEquals x, jwk.get('x') + } + + /** + * Test case discovered during CI testing where a randomly-generated Ed25519 private key with a leading zero byte + * was not being decoded correctly. This test asserts that this value is decoded correctly. + */ + @Test + void testEd25519PrivateJson() { + String use = 'sig' + String kty = 'OKP' + String crv = 'Ed25519' + String x = '9NAzPLMakU0R-tLgX7NmzUUg_fUGiDbrGOWqQ0F_s3g' + String d = 'AAfgb017BkHlLf_SqVBA_LqPhabpdh43dLXHfD6ggQ0' + byte[] decoded = DefaultOctetPrivateJwk.D.applyFrom(d) + assertEquals 0x00, decoded[0] + String json = RfcTests.stripws(""" + { + "use": "$use", + "kty": "$kty", + "crv": "$crv", + "x": "$x", + "d": "$d" + }""") + def jwk = Jwks.parser().build().parse(json) as OctetPrivateJwk + assertEquals use, jwk.getPublicKeyUse() + assertEquals kty, jwk.getType() + assertEquals crv, jwk.get('crv') + assertEquals x, jwk.get('x') + assertEquals d, jwk.get('d').get() // Supplier + def pubJwk = jwk.toPublicJwk() + assertEquals use, pubJwk.getPublicKeyUse() + assertEquals kty, pubJwk.getType() + assertEquals crv, pubJwk.get('crv') + assertEquals x, pubJwk.get('x') + assertNull pubJwk.get('d') + } + + @Test + void testOctetKeyPairs() { + + for (EdwardsCurve curve : EdwardsCurve.VALUES) { + + def pair = curve.keyPairBuilder().build() + PublicKey pub = pair.getPublic() + PrivateKey priv = pair.getPrivate() + + // test individual keys + PublicJwk pubJwk = Jwks.builder().forOctetKey(pub).setPublicKeyUse("sig").build() + PublicJwk pubValuesJwk = Jwks.builder().putAll(pubJwk).build() as PublicJwk // ensure value map symmetry + assertEquals pubJwk, pubValuesJwk + assertEquals pub, pubJwk.toKey() + assertEquals pub, pubValuesJwk.toKey() + + PrivateJwk privJwk = Jwks.builder().forOctetKey(priv).setPublicKey(pub).setPublicKeyUse("sig").build() + PrivateJwk privValuesJwk = Jwks.builder().putAll(privJwk).build() as PrivateJwk // ensure value map symmetry + assertEquals privJwk, privValuesJwk + assertEquals priv, privJwk.toKey() + // we can't assert that priv.equals(privValuesJwk.toKey()) here because BouncyCastle uses PKCS8 V2 encoding + // while the JDK uses V1, and BC implementations check that the encodings are equal (instead of their + // actual key material). Since we only care about the key material for JWK representations, and not the + // key's PKCS8 encoding, we check that their 'd' values are the same, not that the keys' encoding is: + byte[] privMaterial = curve.getKeyMaterial(priv) + byte[] jwkKeyMaterial = curve.getKeyMaterial(privValuesJwk.toKey()) + assertArrayEquals privMaterial, jwkKeyMaterial + + PublicJwk privPubJwk = privJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pubValuesJwk, privPubJwk + assertEquals pub, pubJwk.toKey() + + def jwkPair = privJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + assertEquals priv, jwkPair.getPrivate() + jwkPair = privValuesJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + // see comments above about material equality instead of encoding equality + privMaterial = curve.getKeyMaterial(priv) + jwkKeyMaterial = curve.getKeyMaterial(jwkPair.getPrivate()) + assertArrayEquals privMaterial, jwkKeyMaterial + + // Test public-to-private builder coercion: + privJwk = Jwks.builder().forOctetKey(pub).setPrivateKey(priv).setPublicKeyUse('sig').build() + privValuesJwk = Jwks.builder().putAll(privJwk).build() as PrivateJwk // ensure value map symmetry + assertEquals privJwk, privValuesJwk + assertEquals priv, privJwk.toKey() + // see comments above about material equality instead of encoding equality + privMaterial = curve.getKeyMaterial(priv) + jwkKeyMaterial = curve.getKeyMaterial(jwkPair.getPrivate()) + assertArrayEquals privMaterial, jwkKeyMaterial + + privPubJwk = privJwk.toPublicJwk() + pubValuesJwk = privValuesJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pubJwk, pubValuesJwk + assertEquals pub, pubJwk.toKey() + assertEquals pub, pubValuesJwk.toKey() + + // test pair + privJwk = Jwks.builder().forOctetKeyPair(pair).setPublicKeyUse("sig").build() + assertEquals priv, privJwk.toKey() + // see comments above about material equality instead of encoding equality + privMaterial = curve.getKeyMaterial(priv) + jwkKeyMaterial = curve.getKeyMaterial(privValuesJwk.toKey()) + assertArrayEquals privMaterial, jwkKeyMaterial + + privPubJwk = privJwk.toPublicJwk() + assertEquals pubJwk, privPubJwk + assertEquals pubValuesJwk, privPubJwk + assertEquals pub, pubJwk.toKey() + + jwkPair = privJwk.toKeyPair() + assertEquals pub, jwkPair.getPublic() + assertEquals priv, jwkPair.getPrivate() + } + } + + @Test + void testUnknownCurveId() { + def b = Jwks.builder() + .put(AbstractJwk.KTY.getId(), DefaultOctetPublicJwk.TYPE_VALUE) + .put(DefaultOctetPublicJwk.CRV.getId(), 'foo') + try { + b.build() + fail() + } catch (UnsupportedKeyException e) { + String msg = "Unrecognized OKP JWK ${DefaultOctetPublicJwk.CRV} value 'foo'" as String + assertEquals msg, e.getMessage() + } + } + + /** + * Asserts that a Jwk built with an Edwards Curve private key does not accept an Edwards Curve public key + * on a different curve + */ + @Test + void testPrivateKeyCurvePublicKeyMismatch() { + def priv = TestKeys.X448.pair.private + def mismatchedPub = TestKeys.X25519.pair.public + try { + Jwks.builder().forOctetKey(priv).setPublicKey(mismatchedPub).build() + fail() + } catch (InvalidKeyException ike) { + String msg = "Specified Edwards Curve PublicKey does not match the specified PrivateKey's curve." + assertEquals msg, ike.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PasswordSpecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PasswordSpecTest.groovy new file mode 100644 index 00000000..1cac539d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PasswordSpecTest.groovy @@ -0,0 +1,122 @@ +/* + * 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.security + +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.Password +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.* + +@SuppressWarnings('GroovyAccessibility') +class PasswordSpecTest { + + private char[] PASSWORD + private PasswordSpec KEY + + @Before + void setup() { + PASSWORD = "whatever".toCharArray() + KEY = new PasswordSpec(PASSWORD) + } + + @Test + void testNewInstance() { + assertArrayEquals PASSWORD, KEY.toCharArray() + assertEquals PasswordSpec.NONE_ALGORITHM, KEY.getAlgorithm() + assertNull KEY.getFormat() + } + + @Test + void testGetEncodedUnsupported() { + try { + KEY.getEncoded() + fail() + } catch (UnsupportedOperationException expected) { + assertEquals PasswordSpec.ENCODED_DISABLED_MSG, expected.getMessage() + } + } + + @Test + void testSymmetricChange() { + //assert change in backing array changes key as well: + PASSWORD[0] = 'b' + assertArrayEquals PASSWORD, KEY.toCharArray() + } + + @Test + void testSymmetricDestroy() { + KEY.destroy() + assertTrue KEY.isDestroyed() + for(char c : PASSWORD) { //assert clearing key clears backing array: + assertTrue c == (char)'\u0000' + } + } + + @Test + void testDestroyIdempotent() { + testSymmetricDestroy() + //now do it again to assert idempotent result: + KEY.destroy() + assertTrue KEY.isDestroyed() + for(char c : PASSWORD) { + assertTrue c == (char)'\u0000' + } + } + + @Test + void testDestroyPreventsPassword() { + KEY.destroy() + try { + KEY.toCharArray() + fail() + } catch (IllegalStateException expected) { + assertEquals PasswordSpec.DESTROYED_MSG, expected.getMessage() + } + } + + @Test + void testEquals() { + Password key2 = Keys.forPassword(PASSWORD) + assertArrayEquals KEY.toCharArray(), key2.toCharArray() + assertEquals KEY, key2 + assertNotEquals KEY, new Object() + } + + @Test + void testIdentityEquals() { + Password key = Keys.forPassword(PASSWORD) + assertTrue key.equals(key) + assertNotEquals KEY, new Object() + } + + @Test + void testHashCode() { + Password key2 = Keys.forPassword(PASSWORD) + assertArrayEquals KEY.toCharArray(), key2.toCharArray() + assertEquals KEY.hashCode(), key2.hashCode() + } + + @Test + void testToString() { + assertEquals '', KEY.toString() + Password key2 = Keys.forPassword(PASSWORD) + assertArrayEquals KEY.toCharArray(), key2.toCharArray() + assertEquals KEY.toString(), key2.toString() + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy new file mode 100644 index 00000000..d4be2219 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.JweHeader +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.KeyRequest +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.Password +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +@SuppressWarnings('SpellCheckingInspection') +class Pbes2HsAkwAlgorithmTest { + + private static Password KEY = Keys.forPassword("12345678".toCharArray()) + private static List ALGS = [Jwts.KEY.PBES2_HS256_A128KW, + Jwts.KEY.PBES2_HS384_A192KW, + Jwts.KEY.PBES2_HS512_A256KW] as List + + @Test + void testInsufficientIterations() { + for (Pbes2HsAkwAlgorithm alg : ALGS) { + int iterations = 50 // must be 1000 or more + JweHeader header = Jwts.header().setPbes2Count(iterations).build() as JweHeader + KeyRequest req = new DefaultKeyRequest<>(KEY, null, null, header, Jwts.ENC.A256GCM) + try { + alg.getEncryptionKey(req) + fail() + } catch (IllegalArgumentException iae) { + assertEquals Pbes2HsAkwAlgorithm.MIN_ITERATIONS_MSG_PREFIX + iterations, iae.getMessage() + } + } + } + + // for manual/developer testing only. Takes a long time and there is no deterministic output to assert + /* + @Test + void test() { + + def alg = Jwts.KEY.PBES2_HS256_A128KW + + int desiredMillis = 100 + int iterations = Jwts.KEY.estimateIterations(alg, desiredMillis) + println "Estimated iterations: $iterations" + + int tries = 30 + int skip = 6 + //double scale = 0.5035246727 + + def password = 'hellowor'.toCharArray() + def header = new DefaultJweHeader().setPbes2Count(iterations) + def key = Keys.forPassword(password) + def req = new DefaultKeyRequest(null, null, key, header, Jwts.ENC.A128GCM) + int sum = 0 + for (int i = 0; i < tries; i++) { + long start = System.currentTimeMillis() + alg.getEncryptionKey(req) + long end = System.currentTimeMillis() + long duration = end - start + if (i >= skip) { + sum += duration + } + println "Try $i: ${alg.id} took $duration millis" + } + long avg = Math.round(sum / (tries - skip)) + println "Average duration: $avg" + println "scale factor: ${desiredMillis / avg}" + } + */ +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy new file mode 100644 index 00000000..637868a7 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/PrivateConstructorsTest.groovy @@ -0,0 +1,32 @@ +/* + * 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.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.impl.lang.Functions +import io.jsonwebtoken.lang.Classes +import org.junit.Test + +class PrivateConstructorsTest { + + @Test + void testPrivateCtors() { // for code coverage only + new Classes() + new KeysBridge() + new Conditions() + new Functions() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy new file mode 100644 index 00000000..eea0530c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersTest.groovy @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.lang.Classes +import org.junit.After +import org.junit.Before +import org.junit.Test + +import java.security.Provider +import java.security.Security + +import static org.junit.Assert.* + +class ProvidersTest { + + @Before + void before() { + cleanup() // ensure we start clean + } + + @After + void after() { + cleanup() // ensure we end clean + } + + static void cleanup() { + //ensure test environment is cleaned up: + Providers.BC_PROVIDER.set(null) + Security.removeProvider("BC") + assertFalse bcRegistered() // ensure clean + } + + static boolean bcRegistered() { + for (Provider p : Security.getProviders()) { + // do not reference the Providers class constant here - this is a utility method that could be used in + // other test classes that use static mocks and the `Provider` class might not be able to initialized + if (p.getClass().getName().equals("org.bouncycastle.jce.provider.BouncyCastleProvider")) { + return true + } + } + return false + } + + @Test + void testPrivateCtor() { // for code coverage only + new Providers() + } + + @Test + void testBouncyCastleAlreadyExists() { + + // ensure we don't have one yet: + assertNull Providers.BC_PROVIDER.get() + assertFalse bcRegistered() + + //now register one in the JVM provider list: + Provider bc = Classes.newInstance(Providers.BC_PROVIDER_CLASS_NAME) + assertEquals "BC", bc.getName() + Security.addProvider(bc) + assertTrue bcRegistered() // ensure it exists in the system as expected + + //now ensure that we find it and cache it: + def returned = Providers.findBouncyCastle(Conditions.TRUE) + assertSame bc, returned + assertSame bc, Providers.BC_PROVIDER.get() // ensure cached for future lookup + + //ensure cache hit works: + assertSame bc, Providers.findBouncyCastle(Conditions.TRUE) + + //cleanup() method will remove the provider from the system + } + + @Test + void testBouncyCastleCreatedIfAvailable() { + // ensure we don't have one yet: + assertNull Providers.BC_PROVIDER.get() + assertFalse bcRegistered() + + // ensure we can create one and cache it, *without* modifying the system JVM: + //now ensure that we find it and cache it: + def returned = Providers.findBouncyCastle(Conditions.TRUE) + assertNotNull returned + assertSame Providers.BC_PROVIDER.get(), returned //ensure cached for future lookup + assertFalse bcRegistered() //ensure we don't alter the system environment + + assertSame returned, Providers.findBouncyCastle(Conditions.TRUE) //ensure cache hit + assertFalse bcRegistered() //ensure we don't alter the system environment + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy new file mode 100644 index 00000000..f944e9cb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidersWithoutBCTest.groovy @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.lang.Conditions +import io.jsonwebtoken.lang.Classes +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner + +import static org.easymock.EasyMock.eq +import static org.easymock.EasyMock.expect +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertNull +import static org.powermock.api.easymock.PowerMock.* + +@RunWith(PowerMockRunner.class) +@PrepareForTest([Classes]) +class ProvidersWithoutBCTest { + + @After + void after() { + ProvidersTest.cleanup() //ensure environment is clean + } + + @Test + void testBouncyCastleClassNotAvailable() { + mockStatic(Classes) + expect(Classes.isAvailable(eq("org.bouncycastle.jce.provider.BouncyCastleProvider"))).andReturn(Boolean.FALSE).anyTimes() + replay Classes + assertNull Providers.findBouncyCastle(Conditions.TRUE) // one should not be created/exist + verify Classes + assertFalse ProvidersTest.bcRegistered() // nothing should be in the environment + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy new file mode 100644 index 00000000..2ad7dd1c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA1Test.groovy @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.interfaces.RSAPrivateKey + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +/** + * Tests successful parsing/decryption of a 'JWE using RSAES-OAEP and AES GCM' as defined in + * RFC 7516, Appendix A.1 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7516AppendixA1Test { + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1 : + final static String PLAINTEXT = 'The true sign of intelligence is not knowledge but imagination.' as String + final static byte[] PLAINTEXT_BYTES = [84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32, + 111, 102, 32, 105, 110, 116, 101, 108, 108, 105, 103, 101, 110, 99, + 101, 32, 105, 115, 32, 110, 111, 116, 32, 107, 110, 111, 119, 108, + 101, 100, 103, 101, 32, 98, 117, 116, 32, 105, 109, 97, 103, 105, + 110, 97, 116, 105, 111, 110, 46] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.1 : + final static String PROT_HEADER_STRING = '{"alg":"RSA-OAEP","enc":"A256GCM"}' as String + final static String encodedHeader = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ' as String + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.2 + final static byte[] CEK_BYTES = [177, 161, 244, 128, 84, 143, 225, 115, 63, 180, 3, 255, 107, 154, + 212, 246, 138, 7, 110, 91, 112, 46, 34, 105, 47, 130, 203, 46, 122, + 234, 64, 252] as byte[] + final static SecretKey CEK = new SecretKeySpec(CEK_BYTES, "AES") + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.3 + final static Map KEK_VALUES = [ + "kty": "RSA", + "n" : "oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW" + + "cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S" + + "psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a" + + "sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS" + + "tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj" + + "YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw", + "e" : "AQAB", + "d" : "kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N" + + "WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9" + + "3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk" + + "qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl" + + "t3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd" + + "VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ", + "p" : "1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-" + + "SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf" + + "fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0", + "q" : "wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm" + + "UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX" + + "IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc", + "dp" : "ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL" + + "hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827" + + "rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE", + "dq" : "Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj" + + "ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB" + + "UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis", + "qi" : "VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7" + + "AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3" + + "eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" + ] + + final static byte[] ENCRYPTED_CEK_BYTES = [56, 163, 154, 192, 58, 53, 222, 4, 105, 218, 136, 218, 29, 94, 203, + 22, 150, 92, 129, 94, 211, 232, 53, 89, 41, 60, 138, 56, 196, 216, + 82, 98, 168, 76, 37, 73, 70, 7, 36, 8, 191, 100, 136, 196, 244, 220, + 145, 158, 138, 155, 4, 117, 141, 230, 199, 247, 173, 45, 182, 214, + 74, 177, 107, 211, 153, 11, 205, 196, 171, 226, 162, 128, 171, 182, + 13, 237, 239, 99, 193, 4, 91, 219, 121, 223, 107, 167, 61, 119, 228, + 173, 156, 137, 134, 200, 80, 219, 74, 253, 56, 185, 91, 177, 34, 158, + 89, 154, 205, 96, 55, 18, 138, 43, 96, 218, 215, 128, 124, 75, 138, + 243, 85, 25, 109, 117, 140, 26, 155, 249, 67, 167, 149, 231, 100, 6, + 41, 65, 214, 251, 232, 87, 72, 40, 182, 149, 154, 168, 31, 193, 126, + 215, 89, 28, 111, 219, 125, 182, 139, 235, 195, 197, 23, 234, 55, 58, + 63, 180, 68, 202, 206, 149, 75, 205, 248, 176, 67, 39, 178, 60, 98, + 193, 32, 238, 122, 96, 158, 222, 57, 183, 111, 210, 55, 188, 215, + 206, 180, 166, 150, 166, 106, 250, 55, 229, 72, 40, 69, 214, 216, + 104, 23, 40, 135, 212, 28, 127, 41, 80, 175, 174, 168, 115, 171, 197, + 89, 116, 92, 103, 246, 83, 216, 182, 176, 84, 37, 147, 35, 45, 219, + 172, 99, 226, 233, 73, 37, 124, 42, 72, 49, 242, 35, 127, 184, 134, + 117, 114, 135, 206] as byte[] + + final static String encodedEncryptedCek = 'OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe' + + 'ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb' + + 'Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV' + + 'mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8' + + '1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi' + + '6UklfCpIMfIjf7iGdXKHzg' as String + + // https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.4 + final static byte[] IV = [227, 197, 117, 252, 2, 219, 233, 68, 180, 225, 77, 219] as byte[] + final static String encodedIv = '48V1_ALb6US04U3b' as String + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.5 + final static byte[] AAD_BYTES = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, + 116, 84, 48, 70, 70, 85, 67, 73, 115, 73, 109, 86, 117, 89, 121, 73, + 54, 73, 107, 69, 121, 78, 84, 90, 72, 81, 48, 48, 105, 102, 81] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.6 + final static byte[] CIPHERTEXT = [229, 236, 166, 241, 53, 191, 115, 196, 174, 43, 73, 109, 39, 122, + 233, 96, 140, 206, 120, 52, 51, 237, 48, 11, 190, 219, 186, 80, 111, + 104, 50, 142, 47, 167, 59, 61, 181, 127, 196, 21, 40, 82, 242, 32, + 123, 143, 168, 226, 73, 216, 176, 144, 138, 247, 106, 60, 16, 205, + 160, 109, 64, 63, 192] as byte[] + final static String encodedCiphertext = + '5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A' as String + + final static byte[] TAG = [92, 80, 104, 49, 133, 25, 161, 215, 173, 101, 219, 211, 136, 91, 210, 145] as byte[] + final static String encodedTag = 'XFBoMYUZodetZdvTiFvSkQ' + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.1.7 + final static String COMPLETE_JWE = '' + + 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.' + + 'OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe' + + 'ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb' + + 'Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV' + + 'mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8' + + '1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi' + + '6UklfCpIMfIjf7iGdXKHzg.' + + '48V1_ALb6US04U3b.' + + '5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji' + + 'SdiwkIr3ajwQzaBtQD_A.' + + 'XFBoMYUZodetZdvTiFvSkQ' as String + + @Test + void test() { + //ensure our test constants are correctly copied and match the RFC values: + assertEquals PLAINTEXT, new String(PLAINTEXT_BYTES, StandardCharsets.UTF_8) + assertEquals PROT_HEADER_STRING, new String(decode(encodedHeader), StandardCharsets.UTF_8) + assertEquals encodedEncryptedCek, encode(ENCRYPTED_CEK_BYTES) + assertEquals encodedIv, encode(IV) + assertArrayEquals AAD_BYTES, encodedHeader.getBytes(StandardCharsets.US_ASCII) + assertArrayEquals CIPHERTEXT, decode(encodedCiphertext) + assertArrayEquals TAG, decode(encodedTag) + + //read the RFC Test JWK to get the private key for decrypting + RsaPrivateJwk jwk = Jwks.builder().putAll(KEK_VALUES).build() as RsaPrivateJwk + RSAPrivateKey privKey = jwk.toKey() + + Jwe jwe = Jwts.parserBuilder().decryptWith(privKey).build().parseContentJwe(COMPLETE_JWE) + assertEquals PLAINTEXT, new String(jwe.getPayload(), StandardCharsets.UTF_8) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy new file mode 100644 index 00000000..51f5f196 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA2Test.groovy @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.interfaces.RSAPrivateKey + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +/** + * Tests successful parsing/decryption of a 'JWE using RSAES-PKCS1-v1_5 and AES_128_CBC_HMAC_SHA_256' as defined in + * RFC 7516, Appendix A.2 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7516AppendixA2Test { + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2 : + final static String PLAINTEXT = 'Live long and prosper.' as String + final static byte[] PLAINTEXT_BYTES = [76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32, + 112, 114, 111, 115, 112, 101, 114, 46] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.1 : + final static String PROT_HEADER_STRING = '{"alg":"RSA1_5","enc":"A128CBC-HS256"}' as String + final static String encodedHeader = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0' as String + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.2 + final static byte[] CEK_BYTES = [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, + 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, + 44, 207] as byte[] + final static SecretKey CEK = new SecretKeySpec(CEK_BYTES, "AES") + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.3 + final static Map KEK_VALUES = [ + "kty": "RSA", + "n" : "sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23bOdgWp4Dy1Wl" + + "UzewbgBHod5pcM9H95GQRV3JDXboIRROSBigeC5yjU1hGzHHyXss8UDpre" + + "cbAYxknTcQkhslANGRUZmdTOQ5qTRsLAt6BTYuyvVRdhS8exSZEy_c4gs_" + + "7svlJJQ4H9_NxsiIoLwAEk7-Q3UXERGYw_75IDrGA84-lA_-Ct4eTlXHBI" + + "Y2EaV7t7LjJaynVJCpkv4LKjTTAumiGUIuQhrNhZLuF_RJLqHpM2kgWFLU" + + "7-VTdL1VbC2tejvcI2BlMkEpk1BzBZI0KQB0GaDWFLN-aEAw3vRw", + "e" : "AQAB", + "d" : "VFCWOqXr8nvZNyaaJLXdnNPXZKRaWCjkU5Q2egQQpTBMwhprMzWzpR8Sxq" + + "1OPThh_J6MUD8Z35wky9b8eEO0pwNS8xlh1lOFRRBoNqDIKVOku0aZb-ry" + + "nq8cxjDTLZQ6Fz7jSjR1Klop-YKaUHc9GsEofQqYruPhzSA-QgajZGPbE_" + + "0ZaVDJHfyd7UUBUKunFMScbflYAAOYJqVIVwaYR5zWEEceUjNnTNo_CVSj" + + "-VvXLO5VZfCUAVLgW4dpf1SrtZjSt34YLsRarSb127reG_DUwg9Ch-Kyvj" + + "T1SkHgUWRVGcyly7uvVGRSDwsXypdrNinPA4jlhoNdizK2zF2CWQ", + "p" : "9gY2w6I6S6L0juEKsbeDAwpd9WMfgqFoeA9vEyEUuk4kLwBKcoe1x4HG68" + + "ik918hdDSE9vDQSccA3xXHOAFOPJ8R9EeIAbTi1VwBYnbTp87X-xcPWlEP" + + "krdoUKW60tgs1aNd_Nnc9LEVVPMS390zbFxt8TN_biaBgelNgbC95sM", + "q" : "uKlCKvKv_ZJMVcdIs5vVSU_6cPtYI1ljWytExV_skstvRSNi9r66jdd9-y" + + "BhVfuG4shsp2j7rGnIio901RBeHo6TPKWVVykPu1iYhQXw1jIABfw-MVsN" + + "-3bQ76WLdt2SDxsHs7q7zPyUyHXmps7ycZ5c72wGkUwNOjYelmkiNS0", + "dp" : "w0kZbV63cVRvVX6yk3C8cMxo2qCM4Y8nsq1lmMSYhG4EcL6FWbX5h9yuv" + + "ngs4iLEFk6eALoUS4vIWEwcL4txw9LsWH_zKI-hwoReoP77cOdSL4AVcra" + + "Hawlkpyd2TWjE5evgbhWtOxnZee3cXJBkAi64Ik6jZxbvk-RR3pEhnCs", + "dq" : "o_8V14SezckO6CNLKs_btPdFiO9_kC1DsuUTd2LAfIIVeMZ7jn1Gus_Ff" + + "7B7IVx3p5KuBGOVF8L-qifLb6nQnLysgHDh132NDioZkhH7mI7hPG-PYE_" + + "odApKdnqECHWw0J-F0JWnUd6D2B_1TvF9mXA2Qx-iGYn8OVV1Bsmp6qU", + "qi" : "eNho5yRBEBxhGBtQRww9QirZsB66TrfFReG_CcteI1aCneT0ELGhYlRlC" + + "tUkTRclIfuEPmNsNDPbLoLqqCVznFbvdB7x-Tl-m0l_eFTj2KiqwGqE9PZ" + + "B9nNTwMVvH3VRRSLWACvPnSiwP8N5Usy-WRXS-V7TbpxIhvepTfE0NNo" + ] + + final static byte[] ENCRYPTED_CEK_BYTES = [80, 104, 72, 58, 11, 130, 236, 139, 132, 189, 255, 205, 61, 86, 151, + 176, 99, 40, 44, 233, 176, 189, 205, 70, 202, 169, 72, 40, 226, 181, + 156, 223, 120, 156, 115, 232, 150, 209, 145, 133, 104, 112, 237, 156, + 116, 250, 65, 102, 212, 210, 103, 240, 177, 61, 93, 40, 71, 231, 223, + 226, 240, 157, 15, 31, 150, 89, 200, 215, 198, 203, 108, 70, 117, 66, + 212, 238, 193, 205, 23, 161, 169, 218, 243, 203, 128, 214, 127, 253, + 215, 139, 43, 17, 135, 103, 179, 220, 28, 2, 212, 206, 131, 158, 128, + 66, 62, 240, 78, 186, 141, 125, 132, 227, 60, 137, 43, 31, 152, 199, + 54, 72, 34, 212, 115, 11, 152, 101, 70, 42, 219, 233, 142, 66, 151, + 250, 126, 146, 141, 216, 190, 73, 50, 177, 146, 5, 52, 247, 28, 197, + 21, 59, 170, 247, 181, 89, 131, 241, 169, 182, 246, 99, 15, 36, 102, + 166, 182, 172, 197, 136, 230, 120, 60, 58, 219, 243, 149, 94, 222, + 150, 154, 194, 110, 227, 225, 112, 39, 89, 233, 112, 207, 211, 241, + 124, 174, 69, 221, 179, 107, 196, 225, 127, 167, 112, 226, 12, 242, + 16, 24, 28, 120, 182, 244, 213, 244, 153, 194, 162, 69, 160, 244, + 248, 63, 165, 141, 4, 207, 249, 193, 79, 131, 0, 169, 233, 127, 167, + 101, 151, 125, 56, 112, 111, 248, 29, 232, 90, 29, 147, 110, 169, + 146, 114, 165, 204, 71, 136, 41, 252] as byte[] + + final static String encodedEncryptedCek = 'UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm' + + '1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7Pc' + + 'HALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIF' + + 'NPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8' + + 'rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv' + + '-B3oWh2TbqmScqXMR4gp_A' as String + + // https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.4 + final static byte[] IV = [3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101] as byte[] + final static String encodedIv = 'AxY8DCtDaGlsbGljb3RoZQ' as String + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.5 + final static byte[] AAD_BYTES = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, + 120, 88, 122, 85, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, + 74, 66, 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, + 50, 73, 110, 48] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.6 + final static byte[] CIPHERTEXT = [40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, + 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, + 112, 56, 102] as byte[] + final static String encodedCiphertext = 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY' as String + + final static byte[] TAG = [246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191] as byte[] + final static String encodedTag = '9hH0vgRfYgPnAHOd8stkvw' + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.7 + final static String COMPLETE_JWE = + "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0." + + "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm" + + "1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7Pc" + + "HALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIF" + + "NPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPhcCdZ6XDP0_F8" + + "rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPgwCp6X-nZZd9OHBv" + + "-B3oWh2TbqmScqXMR4gp_A." + + "AxY8DCtDaGlsbGljb3RoZQ." + + "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY." + + "9hH0vgRfYgPnAHOd8stkvw" as String + + @Test + void test() { + //ensure our test constants are correctly copied and match the RFC values: + assertEquals PLAINTEXT, new String(PLAINTEXT_BYTES, StandardCharsets.UTF_8) + assertEquals PROT_HEADER_STRING, new String(decode(encodedHeader), StandardCharsets.UTF_8) + assertEquals encodedEncryptedCek, encode(ENCRYPTED_CEK_BYTES) + assertEquals encodedIv, encode(IV) + assertArrayEquals AAD_BYTES, encodedHeader.getBytes(StandardCharsets.US_ASCII) + assertArrayEquals CIPHERTEXT, decode(encodedCiphertext) + assertArrayEquals TAG, decode(encodedTag) + + //read the RFC Test JWK to get the private key for decrypting + RsaPrivateJwk jwk = Jwks.builder().putAll(KEK_VALUES).build() as RsaPrivateJwk + RSAPrivateKey privKey = jwk.toKey() + + Jwe jwe = Jwts.parserBuilder().decryptWith(privKey).build().parseContentJwe(COMPLETE_JWE) + assertEquals PLAINTEXT, new String(jwe.getPayload(), StandardCharsets.UTF_8) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy new file mode 100644 index 00000000..86b0328f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +class RFC7516AppendixA3Test { + + static String encode(byte[] b) { + return Encoders.BASE64URL.encode(b) + } + + static byte[] decode(String val) { + return Decoders.BASE64URL.decode(val) + } + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3 : + final static String PLAINTEXT = 'Live long and prosper.' as String + final static byte[] PLAINTEXT_BYTES = [76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32, + 112, 114, 111, 115, 112, 101, 114, 46] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.1 : + final static String PROT_HEADER_STRING = '{"alg":"A128KW","enc":"A128CBC-HS256"}' as String + final static String encodedHeader = 'eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0' as String + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.2.2 + final static byte[] CEK_BYTES = [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, + 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, + 44, 207] as byte[] + final static SecretKey CEK = new SecretKeySpec(CEK_BYTES, "AES") + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.3 + final static Map KEK_VALUES = [ + "kty": "oct", + "k" : "GawgguFyGrWKav7AX4VKUg" + ] + + final static byte[] ENCRYPTED_CEK_BYTES = [232, 160, 123, 211, 183, 76, 245, 132, 200, 128, 123, 75, 190, 216, + 22, 67, 201, 138, 193, 186, 9, 91, 122, 31, 246, 90, 28, 139, 57, 3, + 76, 124, 193, 11, 98, 37, 173, 61, 104, 57] as byte[] + + final static String encodedEncryptedCek = '6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ' as String + + // https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.4 + final static byte[] IV = [3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101] as byte[] + final static String encodedIv = 'AxY8DCtDaGlsbGljb3RoZQ' as String + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.5 + final static byte[] AAD_BYTES = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 66, 77, 84, 73, 52, + 83, 49, 99, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, + 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, + 110, 48] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.6 + final static byte[] CIPHERTEXT = [40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, + 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, + 112, 56, 102] as byte[] + final static String encodedCiphertext = 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY' as String + + final static byte[] TAG = [83, 73, 191, 98, 104, 205, 211, 128, 201, 189, 199, 133, 32, 38, 194, 85] as byte[] + final static String encodedTag = 'U0m_YmjN04DJvceFICbCVQ' + + // defined in https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.7 + final static String COMPLETE_JWE = + 'eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.' + + '6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.' + + 'AxY8DCtDaGlsbGljb3RoZQ.' + + 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.' + + 'U0m_YmjN04DJvceFICbCVQ' as String + + @Test + void test() { + //ensure our test constants are correctly copied and match the RFC values: + assertEquals PLAINTEXT, new String(PLAINTEXT_BYTES, StandardCharsets.UTF_8) + assertEquals PROT_HEADER_STRING, new String(decode(encodedHeader), StandardCharsets.UTF_8) + assertEquals encodedEncryptedCek, encode(ENCRYPTED_CEK_BYTES) + assertEquals encodedIv, encode(IV) + assertArrayEquals AAD_BYTES, encodedHeader.getBytes(StandardCharsets.US_ASCII) + assertArrayEquals CIPHERTEXT, decode(encodedCiphertext) + assertArrayEquals TAG, decode(encodedTag) + + //read the RFC Test JWK to get the private key for decrypting + SecretJwk jwk = Jwks.builder().putAll(KEK_VALUES).build() as SecretJwk + SecretKey kek = jwk.toKey() + + // test decryption per the RFC + Jwe jwe = Jwts.parserBuilder().decryptWith(kek).build().parseContentJwe(COMPLETE_JWE) + assertEquals PLAINTEXT, new String(jwe.getPayload(), StandardCharsets.UTF_8) + + // now ensure that when JJWT does the encryption (i.e. a compact value is produced from JJWT, not from the RFC text), + // that the resulting compact string is identical to the RFC as described in + // https://www.rfc-editor.org/rfc/rfc7516.html#appendix-A.3.8 : + + //ensure that the algorithm reflects the test harness values: + AeadAlgorithm enc = new HmacAesAeadAlgorithm(128) { + @Override + protected byte[] ensureInitializationVector(Request request) { + return IV + } + + @Override + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(CEK) + } + } + + String compact = Jwts.builder() + .setPayload(PLAINTEXT) + .encryptWith(kek, Jwts.KEY.A128KW, enc) + .compact() + + assertEquals COMPLETE_JWE, compact + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy new file mode 100644 index 00000000..0f6055b5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA1Test.groovy @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.security.EcPublicJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPublicJwk +import org.junit.Test + +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +/** + * https://www.rfc-editor.org/rfc/rfc7517.html#appendix-A.1 + */ +class RFC7517AppendixA1Test { + + private static final List> keys = [ + [ + "kty": "EC", + "crv": "P-256", + "x" : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y" : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "use": "enc", + "kid": "1" + ], + [ + "kty": "RSA", + "n" : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx" + + "4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs" + + "tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2" + + "QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI" + + "SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb" + + "w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw" as String, + "e" : "AQAB", + "alg": "RS256", + "kid": "2011-04-29" + ] + ] + + @Test + void test() { // asserts we can parse and verify RFC values + + def m = keys[0] + EcPublicJwk ecPubJwk = Jwks.builder().putAll(m).build() as EcPublicJwk + assertTrue ecPubJwk.toKey() instanceof ECPublicKey + assertEquals m.size(), ecPubJwk.size() + assertEquals m.kty, ecPubJwk.getType() + assertEquals m.crv, ecPubJwk.get('crv') + assertEquals m.x, ecPubJwk.get('x') + assertEquals m.y, ecPubJwk.get('y') + assertEquals m.use, ecPubJwk.getPublicKeyUse() + assertEquals m.kid, ecPubJwk.getId() + + m = keys[1] + RsaPublicJwk rsaPublicJwk = Jwks.builder().putAll(m).build() as RsaPublicJwk + assertTrue rsaPublicJwk.toKey() instanceof RSAPublicKey + assertEquals m.size(), rsaPublicJwk.size() + assertEquals m.kty, rsaPublicJwk.getType() + assertEquals m.n, rsaPublicJwk.get('n') + assertEquals m.e, rsaPublicJwk.get('e') + assertEquals m.alg, rsaPublicJwk.getAlgorithm() + assertEquals m.kid, rsaPublicJwk.getId() + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy new file mode 100644 index 00000000..73532a72 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA2Test.groovy @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.impl.lang.Converters +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import org.junit.Test + +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.RSAPrivateCrtKey + +import static org.junit.Assert.* + +/** + * https://www.rfc-editor.org/rfc/rfc7517.html#appendix-A.2 + */ +class RFC7517AppendixA2Test { + + private static final String ecEncode(int fieldSize, BigInteger coord) { + return AbstractEcJwkFactory.toOctetString(fieldSize, coord) + } + + private static final String rsaEncode(BigInteger i) { + return Converters.BIGINT.applyTo(i) as String + } + + private static final List> keys = [ + [ + "kty": "EC", + "crv": "P-256", + "x" : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y" : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d" : "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", + "use": "enc", + "kid": "1" + ], + [ + "kty": "RSA", + "n" : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4" + + "cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMst" + + "n64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2Q" + + "vzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbIS" + + "D08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw" + + "0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e" : "AQAB", + "d" : "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9" + + "M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqij" + + "wp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d" + + "_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBz" + + "nbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFz" + + "me1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", + "p" : "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPV" + + "nwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqV" + + "WlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", + "q" : "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyum" + + "qjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgx" + + "kIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", + "dp" : "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oim" + + "YwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_Nmtu" + + "YZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", + "dq" : "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUU" + + "vMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9" + + "GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", + "qi" : "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzg" + + "UIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rx" + + "yR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", + "alg": "RS256", + "kid": "2011-04-29" + ] + ] + + @Test + void test() { // asserts we can parse and verify RFC values + + def m = keys[0] + def jwk = Jwks.builder().putAll(m).build() as EcPrivateJwk + def key = jwk.toKey() + int fieldSize = key.params.curve.field.fieldSize + assertTrue key instanceof ECPrivateKey + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.crv, jwk.get('crv') + assertEquals m.x, jwk.get('x') + assertEquals m.x, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineX) + assertEquals m.y, jwk.get('y') + assertEquals m.y, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineY) + assertEquals m.d, jwk.get('d').get() + assertEquals m.d, ecEncode(fieldSize, key.s) + assertEquals m.use, jwk.getPublicKeyUse() + assertEquals m.kid, jwk.getId() + + m = keys[1] + jwk = Jwks.builder().putAll(m).build() as RsaPrivateJwk + key = jwk.toKey() as RSAPrivateCrtKey + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.n, jwk.get('n') + assertEquals m.n, rsaEncode(key.modulus) + assertEquals m.e, jwk.get('e') + assertEquals m.e, rsaEncode(jwk.toPublicJwk().toKey().publicExponent) + assertEquals m.d, jwk.get('d').get() + assertEquals m.d, rsaEncode(key.privateExponent) + assertEquals m.p, jwk.get('p').get() + assertEquals m.p, rsaEncode(key.getPrimeP()) + assertEquals m.q, jwk.get('q').get() + assertEquals m.q, rsaEncode(key.getPrimeQ()) + assertEquals m.dp, jwk.get('dp').get() + assertEquals m.dp, rsaEncode(key.getPrimeExponentP()) + assertEquals m.dq, jwk.get('dq').get() + assertEquals m.dq, rsaEncode(key.getPrimeExponentQ()) + assertEquals m.qi, jwk.get('qi').get() + assertEquals m.qi, rsaEncode(key.getCrtCoefficient()) + assertEquals m.alg, jwk.getAlgorithm() + assertEquals m.kid, jwk.getId() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy new file mode 100644 index 00000000..f1893b1e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixA3Test.groovy @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.SecretJwk +import org.junit.Test + +import javax.crypto.SecretKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertNotNull + +/** + * https://www.rfc-editor.org/rfc/rfc7517.html#appendix-A.3 + */ +class RFC7517AppendixA3Test { + + private static final String encode(SecretKey key) { + return Encoders.BASE64URL.encode(key.getEncoded()) + } + + private static final List> keys = [ + [ + "kty": "oct", + "alg": "A128KW", + "k" : "GawgguFyGrWKav7AX4VKUg" + ], + [ + "kty": "oct", + "k" : "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", + "kid": "HMAC key used in JWS spec Appendix A.1 example" + ] + ] + + @Test + void test() { // asserts we can parse and verify RFC values + + def m = keys[0] + SecretJwk jwk = Jwks.builder().putAll(m).build() as SecretJwk + def key = jwk.toKey() as SecretKey + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.alg, jwk.getAlgorithm() + assertEquals m.k, jwk.get('k').get() + assertEquals m.k, encode(key) + + m = keys[1] + jwk = Jwks.builder().putAll(m).build() as SecretJwk + key = jwk.toKey() as SecretKey + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.k, jwk.get('k').get() + assertEquals m.k, encode(key) + assertEquals m.kid, jwk.getId() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy new file mode 100644 index 00000000..aaee5844 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixBTest.groovy @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.impl.lang.Converters +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPublicJwk +import org.junit.Test + +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +/** + * https://www.rfc-editor.org/rfc/rfc7517.html#appendix-B + */ +class RFC7517AppendixBTest { + + private static final Map jwkPairs = [ + "kty": "RSA", + "use": "sig", + "kid": "1b94c", + "n" : "vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08" + + "PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Q" + + "u2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4a" + + "YWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwH" + + "MTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMv" + + "VfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ", + "e" : "AQAB", + "x5c": [ + "MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJB" + + "gNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYD" + + "VQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1" + + "wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBg" + + "NVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDV" + + "QQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1w" + + "YmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnH" + + "YMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66" + + "s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6" + + "SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpn" + + "fajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPq" + + "PvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVk" + + "aZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BA" + + "QUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL" + + "+9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1" + + "zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL" + + "2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo" + + "4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTq" + + "gawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA==" + ] + ] + + @Test + void test() { + def m = jwkPairs + RsaPublicJwk jwk = Jwks.builder().putAll(m).build() as RsaPublicJwk + RSAPublicKey key = jwk.toKey() + assertNotNull key + assertEquals m.size(), jwk.size() + assertEquals m.kty, jwk.getType() + assertEquals m.use, jwk.getPublicKeyUse() + assertEquals m.kid, jwk.getId() + assertEquals m.n, Converters.BIGINT.applyTo(key.getModulus()) + assertEquals m.e, Converters.BIGINT.applyTo(key.getPublicExponent()) + def chain = jwk.getX509CertificateChain() + assertNotNull chain + assertFalse chain.isEmpty() + assertEquals 1, chain.size() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy new file mode 100644 index 00000000..c74c666c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.JweHeader +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.io.SerializationException +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.Key + +import static org.junit.Assert.* + +/** + * https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C + */ +@SuppressWarnings('SpellCheckingInspection') +class RFC7517AppendixCTest { + + private static final String rfcString(String s) { + return s.replaceAll('[\\s]', '') + } + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.1 + private static final String RFC_JWK_JSON = rfcString(''' + { + "kty":"RSA", + "kid":"juliet@capulet.lit", + "use":"enc", + "n":"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy + O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP + 8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0 + Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X + OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1 + _I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q", + "e":"AQAB", + "d":"GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS + NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U + vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu + ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu + rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a + hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ", + "p":"2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf + QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8 + UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws", + "q":"1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I + edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK + rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s", + "dp":"KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3 + tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w + Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c", + "dq":"AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9 + GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy + mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots", + "qi":"lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq + abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o + Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8" + } + ''') + + private static final byte[] RFC_JWK_JSON_BYTES = + [123, 34, 107, 116, 121, 34, 58, 34, 82, 83, 65, 34, 44, 34, 107, + 105, 100, 34, 58, 34, 106, 117, 108, 105, 101, 116, 64, 99, 97, 112, + 117, 108, 101, 116, 46, 108, 105, 116, 34, 44, 34, 117, 115, 101, 34, + 58, 34, 101, 110, 99, 34, 44, 34, 110, 34, 58, 34, 116, 54, 81, 56, + 80, 87, 83, 105, 49, 100, 107, 74, 106, 57, 104, 84, 80, 56, 104, 78, + 89, 70, 108, 118, 97, 100, 77, 55, 68, 102, 108, 87, 57, 109, 87, + 101, 112, 79, 74, 104, 74, 54, 54, 119, 55, 110, 121, 111, 75, 49, + 103, 80, 78, 113, 70, 77, 83, 81, 82, 121, 79, 49, 50, 53, 71, 112, + 45, 84, 69, 107, 111, 100, 104, 87, 114, 48, 105, 117, 106, 106, 72, + 86, 120, 55, 66, 99, 86, 48, 108, 108, 83, 52, 119, 53, 65, 67, 71, + 103, 80, 114, 99, 65, 100, 54, 90, 99, 83, 82, 48, 45, 73, 113, 111, + 109, 45, 81, 70, 99, 78, 80, 56, 83, 106, 103, 48, 56, 54, 77, 119, + 111, 113, 81, 85, 95, 76, 89, 121, 119, 108, 65, 71, 90, 50, 49, 87, + 83, 100, 83, 95, 80, 69, 82, 121, 71, 70, 105, 78, 110, 106, 51, 81, + 81, 108, 79, 56, 89, 110, 115, 53, 106, 67, 116, 76, 67, 82, 119, 76, + 72, 76, 48, 80, 98, 49, 102, 69, 118, 52, 53, 65, 117, 82, 73, 117, + 85, 102, 86, 99, 80, 121, 83, 66, 87, 89, 110, 68, 121, 71, 120, 118, + 106, 89, 71, 68, 83, 77, 45, 65, 113, 87, 83, 57, 122, 73, 81, 50, + 90, 105, 108, 103, 84, 45, 71, 113, 85, 109, 105, 112, 103, 48, 88, + 79, 67, 48, 67, 99, 50, 48, 114, 103, 76, 101, 50, 121, 109, 76, 72, + 106, 112, 72, 99, 105, 67, 75, 86, 65, 98, 89, 53, 45, 76, 51, 50, + 45, 108, 83, 101, 90, 79, 45, 79, 115, 54, 85, 49, 53, 95, 97, 88, + 114, 107, 57, 71, 119, 56, 99, 80, 85, 97, 88, 49, 95, 73, 56, 115, + 76, 71, 117, 83, 105, 86, 100, 116, 51, 67, 95, 70, 110, 50, 80, 90, + 51, 90, 56, 105, 55, 52, 52, 70, 80, 70, 71, 71, 99, 71, 49, 113, + 115, 50, 87, 122, 45, 81, 34, 44, 34, 101, 34, 58, 34, 65, 81, 65, + 66, 34, 44, 34, 100, 34, 58, 34, 71, 82, 116, 98, 73, 81, 109, 104, + 79, 90, 116, 121, 115, 122, 102, 103, 75, 100, 103, 52, 117, 95, 78, + 45, 82, 95, 109, 90, 71, 85, 95, 57, 107, 55, 74, 81, 95, 106, 110, + 49, 68, 110, 102, 84, 117, 77, 100, 83, 78, 112, 114, 84, 101, 97, + 83, 84, 121, 87, 102, 83, 78, 107, 117, 97, 65, 119, 110, 79, 69, 98, + 73, 81, 86, 121, 49, 73, 81, 98, 87, 86, 86, 50, 53, 78, 89, 51, 121, + 98, 99, 95, 73, 104, 85, 74, 116, 102, 114, 105, 55, 98, 65, 88, 89, + 69, 82, 101, 87, 97, 67, 108, 51, 104, 100, 108, 80, 75, 88, 121, 57, + 85, 118, 113, 80, 89, 71, 82, 48, 107, 73, 88, 84, 81, 82, 113, 110, + 115, 45, 100, 86, 74, 55, 106, 97, 104, 108, 73, 55, 76, 121, 99, + 107, 114, 112, 84, 109, 114, 77, 56, 100, 87, 66, 111, 52, 95, 80, + 77, 97, 101, 110, 78, 110, 80, 105, 81, 103, 79, 48, 120, 110, 117, + 84, 111, 120, 117, 116, 82, 90, 74, 102, 74, 118, 71, 52, 79, 120, + 52, 107, 97, 51, 71, 79, 82, 81, 100, 57, 67, 115, 67, 90, 50, 118, + 115, 85, 68, 109, 115, 88, 79, 102, 85, 69, 78, 79, 121, 77, 113, 65, + 68, 67, 54, 112, 49, 77, 51, 104, 51, 51, 116, 115, 117, 114, 89, 49, + 53, 107, 57, 113, 77, 83, 112, 71, 57, 79, 88, 95, 73, 74, 65, 88, + 109, 120, 122, 65, 104, 95, 116, 87, 105, 90, 79, 119, 107, 50, 75, + 52, 121, 120, 72, 57, 116, 83, 51, 76, 113, 49, 121, 88, 56, 67, 49, + 69, 87, 109, 101, 82, 68, 107, 75, 50, 97, 104, 101, 99, 71, 56, 53, + 45, 111, 76, 75, 81, 116, 53, 86, 69, 112, 87, 72, 75, 109, 106, 79, + 105, 95, 103, 74, 83, 100, 83, 103, 113, 99, 78, 57, 54, 88, 53, 50, + 101, 115, 65, 81, 34, 44, 34, 112, 34, 58, 34, 50, 114, 110, 83, 79, + 86, 52, 104, 75, 83, 78, 56, 115, 83, 52, 67, 103, 99, 81, 72, 70, + 98, 115, 48, 56, 88, 98, 111, 70, 68, 113, 75, 117, 109, 51, 115, 99, + 52, 104, 51, 71, 82, 120, 114, 84, 109, 81, 100, 108, 49, 90, 75, 57, + 117, 119, 45, 80, 73, 72, 102, 81, 80, 48, 70, 107, 120, 88, 86, 114, + 120, 45, 87, 69, 45, 90, 69, 98, 114, 113, 105, 118, 72, 95, 50, 105, + 67, 76, 85, 83, 55, 119, 65, 108, 54, 88, 118, 65, 82, 116, 49, 75, + 107, 73, 97, 85, 120, 80, 80, 83, 89, 66, 57, 121, 107, 51, 49, 115, + 48, 81, 56, 85, 75, 57, 54, 69, 51, 95, 79, 114, 65, 68, 65, 89, 116, + 65, 74, 115, 45, 77, 51, 74, 120, 67, 76, 102, 78, 103, 113, 104, 53, + 54, 72, 68, 110, 69, 84, 84, 81, 104, 72, 51, 114, 67, 84, 53, 84, + 51, 121, 74, 119, 115, 34, 44, 34, 113, 34, 58, 34, 49, 117, 95, 82, + 105, 70, 68, 80, 55, 76, 66, 89, 104, 51, 78, 52, 71, 88, 76, 84, 57, + 79, 112, 83, 75, 89, 80, 48, 117, 81, 90, 121, 105, 97, 90, 119, 66, + 116, 79, 67, 66, 78, 74, 103, 81, 120, 97, 106, 49, 48, 82, 87, 106, + 115, 90, 117, 48, 99, 54, 73, 101, 100, 105, 115, 52, 83, 55, 66, 95, + 99, 111, 83, 75, 66, 48, 75, 106, 57, 80, 97, 80, 97, 66, 122, 103, + 45, 73, 121, 83, 82, 118, 118, 99, 81, 117, 80, 97, 109, 81, 117, 54, + 54, 114, 105, 77, 104, 106, 86, 116, 71, 54, 84, 108, 86, 56, 67, 76, + 67, 89, 75, 114, 89, 108, 53, 50, 122, 105, 113, 75, 48, 69, 95, 121, + 109, 50, 81, 110, 107, 119, 115, 85, 88, 55, 101, 89, 84, 66, 55, 76, + 98, 65, 72, 82, 75, 57, 71, 113, 111, 99, 68, 69, 53, 66, 48, 102, + 56, 48, 56, 73, 52, 115, 34, 44, 34, 100, 112, 34, 58, 34, 75, 107, + 77, 84, 87, 113, 66, 85, 101, 102, 86, 119, 90, 50, 95, 68, 98, 106, + 49, 112, 80, 81, 113, 121, 72, 83, 72, 106, 106, 57, 48, 76, 53, 120, + 95, 77, 79, 122, 113, 89, 65, 74, 77, 99, 76, 77, 90, 116, 98, 85, + 116, 119, 75, 113, 118, 86, 68, 113, 51, 116, 98, 69, 111, 51, 90, + 73, 99, 111, 104, 98, 68, 116, 116, 54, 83, 98, 102, 109, 87, 122, + 103, 103, 97, 98, 112, 81, 120, 78, 120, 117, 66, 112, 111, 79, 79, + 102, 95, 97, 95, 72, 103, 77, 88, 75, 95, 108, 104, 113, 105, 103, + 73, 52, 121, 95, 107, 113, 83, 49, 119, 89, 53, 50, 73, 119, 106, 85, + 110, 53, 114, 103, 82, 114, 74, 45, 121, 89, 111, 49, 104, 52, 49, + 75, 82, 45, 118, 122, 50, 112, 89, 104, 69, 65, 101, 89, 114, 104, + 116, 116, 87, 116, 120, 86, 113, 76, 67, 82, 86, 105, 68, 54, 99, 34, + 44, 34, 100, 113, 34, 58, 34, 65, 118, 102, 83, 48, 45, 103, 82, 120, + 118, 110, 48, 98, 119, 74, 111, 77, 83, 110, 70, 120, 89, 99, 75, 49, + 87, 110, 117, 69, 106, 81, 70, 108, 117, 77, 71, 102, 119, 71, 105, + 116, 81, 66, 87, 116, 102, 90, 49, 69, 114, 55, 116, 49, 120, 68, + 107, 98, 78, 57, 71, 81, 84, 66, 57, 121, 113, 112, 68, 111, 89, 97, + 78, 48, 54, 72, 55, 67, 70, 116, 114, 107, 120, 104, 74, 73, 66, 81, + 97, 106, 54, 110, 107, 70, 53, 75, 75, 83, 51, 84, 81, 116, 81, 53, + 113, 67, 122, 107, 79, 107, 109, 120, 73, 101, 51, 75, 82, 98, 66, + 121, 109, 88, 120, 107, 98, 53, 113, 119, 85, 112, 88, 53, 69, 76, + 68, 53, 120, 70, 99, 54, 70, 101, 105, 97, 102, 87, 89, 89, 54, 51, + 84, 109, 109, 69, 65, 117, 95, 108, 82, 70, 67, 79, 74, 51, 120, 68, + 101, 97, 45, 111, 116, 115, 34, 44, 34, 113, 105, 34, 58, 34, 108, + 83, 81, 105, 45, 119, 57, 67, 112, 121, 85, 82, 101, 77, 69, 114, 80, + 49, 82, 115, 66, 76, 107, 55, 119, 78, 116, 79, 118, 115, 53, 69, 81, + 112, 80, 113, 109, 117, 77, 118, 113, 87, 53, 55, 78, 66, 85, 99, + 122, 83, 99, 69, 111, 80, 119, 109, 85, 113, 113, 97, 98, 117, 57, + 86, 48, 45, 80, 121, 52, 100, 81, 53, 55, 95, 98, 97, 112, 111, 75, + 82, 117, 49, 82, 57, 48, 98, 118, 117, 70, 110, 85, 54, 51, 83, 72, + 87, 69, 70, 103, 108, 90, 81, 118, 74, 68, 77, 101, 65, 118, 109, + 106, 52, 115, 109, 45, 70, 112, 48, 111, 89, 117, 95, 110, 101, 111, + 116, 103, 81, 48, 104, 122, 98, 73, 53, 103, 114, 121, 55, 97, 106, + 100, 89, 121, 57, 45, 50, 108, 78, 120, 95, 55, 54, 97, 66, 90, 111, + 79, 85, 117, 57, 72, 67, 74, 45, 85, 115, 102, 83, 79, 73, 56, 34, + 125] as byte[] + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.2 + private static final String RFC_JWE_PROTECTED_HEADER_JSON = rfcString(''' + { + "alg":"PBES2-HS256+A128KW", + "p2s":"2WCTcJZ1Rvd_CJuJripQ1w", + "p2c":4096, + "enc":"A128CBC-HS256", + "cty":"jwk+json" + } + ''') + private static final byte[] RFC_P2S = [217, 96, 147, 112, 150, 117, 70, 247, 127, 8, 155, 137, 174, 42, 80, 215] as byte[] + private static final int RFC_P2C = 4096 + private static final String RFC_ENCODED_JWE_PROTECTED_HEADER = rfcString(''' + eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSn + VKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5Ijoi + andrK2pzb24ifQ + ''') + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.3 + private static final byte[] RFC_CEK_BYTES = [111, 27, 25, 52, 66, 29, 20, 78, 92, 176, 56, 240, 65, 208, 82, 112, + 161, 131, 36, 55, 202, 236, 185, 172, 129, 23, 153, 194, 195, 48, + 253, 182] as byte[] + private static final SecretKey RFC_CEK = new SecretKeySpec(RFC_CEK_BYTES, "AES") + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.4 + private static final String RFC_SHARED_PASSPHRASE = 'Thus from my lips, by yours, my sin is purged.' + private static final byte[] RFC_SHARED_PASSPHRASE_BYTES = [ + 84, 104, 117, 115, 32, 102, 114, 111, 109, 32, 109, 121, 32, 108, + 105, 112, 115, 44, 32, 98, 121, 32, 121, 111, 117, 114, 115, 44, 32, + 109, 121, 32, 115, 105, 110, 32, 105, 115, 32, 112, 117, 114, 103, + 101, 100, 46] as byte[] + + // "The Salt value (UTF8(Alg) || 0x00 || Salt Input) is": + @SuppressWarnings('unused') + private static final byte[] RFC_SALT_VALUE = [80, 66, 69, 83, 50, 45, 72, 83, 50, 53, 54, 43, 65, 49, 50, 56, 75, + 87, 0, 217, 96, 147, 112, 150, 117, 70, 247, 127, 8, 155, 137, 174, + 42, 80, 215] as byte[] + @SuppressWarnings('unused') + private static final byte[] RFC_PBKDF2_DERIVED_KEY_BYTES = + [110, 171, 169, 92, 129, 92, 109, 117, 233, 242, 116, 233, 170, 14, 24, 75] + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.6 + private static final byte[] RFC_IV = [97, 239, 99, 214, 171, 54, 216, 57, 145, 72, 7, 93, 34, 31, 149, 156] as byte[] + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.9 + private static final String RFC_COMPACT_JWE = rfcString(''' + eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSn + VKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5Ijoi + andrK2pzb24ifQ. + TrqXOwuNUfDV9VPTNbyGvEJ9JMjefAVn-TR1uIxR9p6hsRQh9Tk7BA. + Ye9j1qs22DmRSAddIh-VnA. + AwhB8lxrlKjFn02LGWEqg27H4Tg9fyZAbFv3p5ZicHpj64QyHC44qqlZ3JEmnZTgQo + wIqZJ13jbyHB8LgePiqUJ1hf6M2HPLgzw8L-mEeQ0jvDUTrE07NtOerBk8bwBQyZ6g + 0kQ3DEOIglfYxV8-FJvNBYwbqN1Bck6d_i7OtjSHV-8DIrp-3JcRIe05YKy3Oi34Z_ + GOiAc1EK21B11c_AE11PII_wvvtRiUiG8YofQXakWd1_O98Kap-UgmyWPfreUJ3lJP + nbD4Ve95owEfMGLOPflo2MnjaTDCwQokoJ_xplQ2vNPz8iguLcHBoKllyQFJL2mOWB + wqhBo9Oj-O800as5mmLsvQMTflIrIEbbTMzHMBZ8EFW9fWwwFu0DWQJGkMNhmBZQ-3 + lvqTc-M6-gWA6D8PDhONfP2Oib2HGizwG1iEaX8GRyUpfLuljCLIe1DkGOewhKuKkZ + h04DKNM5Nbugf2atmU9OP0Ldx5peCUtRG1gMVl7Qup5ZXHTjgPDr5b2N731UooCGAU + qHdgGhg0JVJ_ObCTdjsH4CF1SJsdUhrXvYx3HJh2Xd7CwJRzU_3Y1GxYU6-s3GFPbi + rfqqEipJDBTHpcoCmyrwYjYHFgnlqBZRotRrS95g8F95bRXqsaDY7UgQGwBQBwy665 + d0zpvTasvfXf_c0MWAl-neFaKOW_Px6g4EUDjG1GWSXV9cLStLw_0ovdApDIFLHYHe + PyagyHjouQUuGiq7BsYwYrwaF06tgB8hV8omLNfMEmDPJaZUzMuHw6tBDwGkzD-tS_ + ub9hxrpJ4UsOWnt5rGUyoN2N_c1-TQlXxm5oto14MxnoAyBQBpwIEgSH3Y4ZhwKBhH + PjSo0cdwuNdYbGPpb-YUvF-2NZzODiQ1OvWQBRHSbPWYz_xbGkgD504LRtqRwCO7CC + _CyyURi1sEssPVsMJRX_U4LFEOc82TiDdqjKOjRUfKK5rqLi8nBE9soQ0DSaOoFQZi + GrBrqxDsNYiAYAmxxkos-i3nX4qtByVx85sCE5U_0MqG7COxZWMOPEFrDaepUV-cOy + rvoUIng8i8ljKBKxETY2BgPegKBYCxsAUcAkKamSCC9AiBxA0UOHyhTqtlvMksO7AE + hNC2-YzPyx1FkhMoS4LLe6E_pFsMlmjA6P1NSge9C5G5tETYXGAn6b1xZbHtmwrPSc + ro9LWhVmAaA7_bxYObnFUxgWtK4vzzQBjZJ36UTk4OTB-JvKWgfVWCFsaw5WCHj6Oo + 4jpO7d2yN7WMfAj2hTEabz9wumQ0TMhBduZ-QON3pYObSy7TSC1vVme0NJrwF_cJRe + hKTFmdlXGVldPxZCplr7ZQqRQhF8JP-l4mEQVnCaWGn9ONHlemczGOS-A-wwtnmwjI + B1V_vgJRf4FdpV-4hUk4-QLpu3-1lWFxrtZKcggq3tWTduRo5_QebQbUUT_VSCgsFc + OmyWKoj56lbxthN19hq1XGWbLGfrrR6MWh23vk01zn8FVwi7uFwEnRYSafsnWLa1Z5 + TpBj9GvAdl2H9NHwzpB5NqHpZNkQ3NMDj13Fn8fzO0JB83Etbm_tnFQfcb13X3bJ15 + Cz-Ww1MGhvIpGGnMBT_ADp9xSIyAM9dQ1yeVXk-AIgWBUlN5uyWSGyCxp0cJwx7HxM + 38z0UIeBu-MytL-eqndM7LxytsVzCbjOTSVRmhYEMIzUAnS1gs7uMQAGRdgRIElTJE + SGMjb_4bZq9s6Ve1LKkSi0_QDsrABaLe55UY0zF4ZSfOV5PMyPtocwV_dcNPlxLgNA + D1BFX_Z9kAdMZQW6fAmsfFle0zAoMe4l9pMESH0JB4sJGdCKtQXj1cXNydDYozF7l8 + H00BV_Er7zd6VtIw0MxwkFCTatsv_R-GsBCH218RgVPsfYhwVuT8R4HarpzsDBufC4 + r8_c8fc9Z278sQ081jFjOja6L2x0N_ImzFNXU6xwO-Ska-QeuvYZ3X_L31ZOX4Llp- + 7QSfgDoHnOxFv1Xws-D5mDHD3zxOup2b2TppdKTZb9eW2vxUVviM8OI9atBfPKMGAO + v9omA-6vv5IxUH0-lWMiHLQ_g8vnswp-Jav0c4t6URVUzujNOoNd_CBGGVnHiJTCHl + 88LQxsqLHHIu4Fz-U2SGnlxGTj0-ihit2ELGRv4vO8E1BosTmf0cx3qgG0Pq0eOLBD + IHsrdZ_CCAiTc0HVkMbyq1M6qEhM-q5P6y1QCIrwg. + 0HFmhOzsQ98nNWJjIHkR7A +''') + + @Test + void test() { + + //ensure the bytes of the JSON string copied from the RFC matches the array definition in the RFC: + assertArrayEquals RFC_JWK_JSON_BYTES, RFC_JWK_JSON.getBytes(StandardCharsets.ISO_8859_1) + assertEquals RFC_ENCODED_JWE_PROTECTED_HEADER, Encoders.BASE64URL.encode(RFC_JWE_PROTECTED_HEADER_JSON.getBytes(StandardCharsets.UTF_8)) + assertArrayEquals RFC_SHARED_PASSPHRASE_BYTES, RFC_SHARED_PASSPHRASE.getBytes(StandardCharsets.UTF_8) + + //ensure that the KeyAlgorithm reflects test harness values: + def enc = new HmacAesAeadAlgorithm(128) { + @Override + SecretKeyBuilder keyBuilder() { + return new FixedSecretKeyBuilder(RFC_CEK) + } + + @Override + protected byte[] ensureInitializationVector(Request request) { + return RFC_IV + } + } + def alg = new Pbes2HsAkwAlgorithm(128) { + @Override + protected byte[] generateInputSalt(KeyRequest request) { + return RFC_P2S + } + } + def serializer = new Serializer() { + @Override + byte[] serialize(Object o) throws SerializationException { + assertTrue o instanceof JweHeader + JweHeader header = (JweHeader) o + + //assert the 5 values have been set per the RFC: + assertEquals 5, header.size() + assertEquals 'PBES2-HS256+A128KW', header.getAlgorithm() + assertEquals '2WCTcJZ1Rvd_CJuJripQ1w', header.p2s + assertEquals 4096, header.p2c + assertEquals 'A128CBC-HS256', header.getEncryptionAlgorithm() + assertEquals 'jwk+json', header.cty + + //JSON serialization order isn't guaranteed, so now that we've asserted the values are correct, + //return the exact serialization order expected in the RFC test: + return RFC_JWE_PROTECTED_HEADER_JSON.getBytes(StandardCharsets.UTF_8) + } + } + + Password key = Keys.forPassword(RFC_SHARED_PASSPHRASE.toCharArray()) + + String compact = Jwts.builder() + .setPayload(RFC_JWK_JSON) + .setHeader(Jwts.header().setContentType('jwk+json').setPbes2Count(RFC_P2C)) + .encryptWith(key, alg, enc) + .serializeToJsonWith(serializer) //ensure header created as expected with an assertion serializer + .compact() + + assertEquals RFC_COMPACT_JWE, compact + + //ensure we can decrypt now: + Jwe jwe = Jwts.parserBuilder().decryptWith(key).build().parseContentJwe(compact) + + assertEquals RFC_JWK_JSON, new String(jwe.getPayload(), StandardCharsets.UTF_8) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy new file mode 100644 index 00000000..fc47e1ff --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.Jwts +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals + +/** + * Tests successful encryption and decryption using 'AES_128_CBC_HMAC_SHA_256' as defined in + * RFC 7518, Appendix B.1 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7518AppendixB1Test { + + final byte[] K = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + @SuppressWarnings('unused') + final byte[] MAC_KEY = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f] as byte[] + + @SuppressWarnings('unused') + final byte[] ENC_KEY = + [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f] as byte[] + + final byte[] P = + [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[] + + final byte[] IV = + [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[] + + final byte[] A = + [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + + @SuppressWarnings('unused') + final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] + + final byte[] E = + [0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79, + 0xa2, 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9, + 0xa9, 0x4a, 0xc9, 0xb4, 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2, + 0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36, + 0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8, + 0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, 0xb9, 0xcf, 0x1b, + 0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f, + 0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5, + 0x4b, 0x88, 0x51, 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb] as byte[] + + @SuppressWarnings('unused') + final byte[] M = + [0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4, + 0xe6, 0xe5, 0x45, 0x82, 0x47, 0x65, 0x15, 0xf0, 0xad, 0x9f, 0x75, 0xa2, 0xb7, 0x1c, 0x73, 0xef] as byte[] + + final byte[] T = + [0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4] as byte[] + + @Test + void test() { + + def alg = Jwts.ENC.A128CBC_HS256 + def request = new DefaultAeadRequest(P, null, null, KEY, A, IV) + def result = alg.encrypt(request) + + byte[] ciphertext = result.getPayload() + byte[] tag = result.getDigest() + byte[] iv = result.getInitializationVector() + + assertArrayEquals E, ciphertext + assertArrayEquals T, tag + assertArrayEquals IV, iv //shouldn't have been altered + + // now test decryption: + def dreq = new DefaultAeadResult(null, null, ciphertext, KEY, A, tag, iv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() + assertArrayEquals(P, decryptionResult) + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy new file mode 100644 index 00000000..414dc6cd --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.AeadRequest +import io.jsonwebtoken.security.AeadResult +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals + +/** + * Tests successful encryption and decryption using 'AES_192_CBC_HMAC_SHA_384' as defined in + * RFC 7518, Appendix B.2 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7518AppendixB2Test { + + final byte[] K = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] P = + [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[] + + final byte[] IV = + [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[] + + final byte[] A = + [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + + @SuppressWarnings('unused') + final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] + + final byte[] E = + [0xea, 0x65, 0xda, 0x6b, 0x59, 0xe6, 0x1e, 0xdb, 0x41, 0x9b, 0xe6, 0x2d, 0x19, 0x71, 0x2a, 0xe5, + 0xd3, 0x03, 0xee, 0xb5, 0x00, 0x52, 0xd0, 0xdf, 0xd6, 0x69, 0x7f, 0x77, 0x22, 0x4c, 0x8e, 0xdb, + 0x00, 0x0d, 0x27, 0x9b, 0xdc, 0x14, 0xc1, 0x07, 0x26, 0x54, 0xbd, 0x30, 0x94, 0x42, 0x30, 0xc6, + 0x57, 0xbe, 0xd4, 0xca, 0x0c, 0x9f, 0x4a, 0x84, 0x66, 0xf2, 0x2b, 0x22, 0x6d, 0x17, 0x46, 0x21, + 0x4b, 0xf8, 0xcf, 0xc2, 0x40, 0x0a, 0xdd, 0x9f, 0x51, 0x26, 0xe4, 0x79, 0x66, 0x3f, 0xc9, 0x0b, + 0x3b, 0xed, 0x78, 0x7a, 0x2f, 0x0f, 0xfc, 0xbf, 0x39, 0x04, 0xbe, 0x2a, 0x64, 0x1d, 0x5c, 0x21, + 0x05, 0xbf, 0xe5, 0x91, 0xba, 0xe2, 0x3b, 0x1d, 0x74, 0x49, 0xe5, 0x32, 0xee, 0xf6, 0x0a, 0x9a, + 0xc8, 0xbb, 0x6c, 0x6b, 0x01, 0xd3, 0x5d, 0x49, 0x78, 0x7b, 0xcd, 0x57, 0xef, 0x48, 0x49, 0x27, + 0xf2, 0x80, 0xad, 0xc9, 0x1a, 0xc0, 0xc4, 0xe7, 0x9c, 0x7b, 0x11, 0xef, 0xc6, 0x00, 0x54, 0xe3] as byte[] + + @SuppressWarnings('unused') + final byte[] M = + [0x84, 0x90, 0xac, 0x0e, 0x58, 0x94, 0x9b, 0xfe, 0x51, 0x87, 0x5d, 0x73, 0x3f, 0x93, 0xac, 0x20, + 0x75, 0x16, 0x80, 0x39, 0xcc, 0xc7, 0x33, 0xd7, 0x45, 0x94, 0xf8, 0x86, 0xb3, 0xfa, 0xaf, 0xd4, + 0x86, 0xf2, 0x5c, 0x71, 0x31, 0xe3, 0x28, 0x1e, 0x36, 0xc7, 0xa2, 0xd1, 0x30, 0xaf, 0xde, 0x57] as byte[] + + final byte[] T = + [0x84, 0x90, 0xac, 0x0e, 0x58, 0x94, 0x9b, 0xfe, 0x51, 0x87, 0x5d, 0x73, 0x3f, 0x93, 0xac, 0x20, + 0x75, 0x16, 0x80, 0x39, 0xcc, 0xc7, 0x33, 0xd7] as byte[] + + @Test + void test() { + + def alg = Jwts.ENC.A192CBC_HS384 + AeadRequest req = new DefaultAeadRequest(P, null, null, KEY, A, IV) + AeadResult result = alg.encrypt(req) + + byte[] resultCiphertext = result.getPayload() + byte[] resultTag = result.getDigest() + byte[] resultIv = result.getInitializationVector() + + assertArrayEquals E, resultCiphertext + assertArrayEquals T, resultTag + assertArrayEquals IV, resultIv //shouldn't have been altered + + // now test decryption: + def dreq = new DefaultAeadResult(null, null, resultCiphertext, KEY, A, resultTag, resultIv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() + assertArrayEquals(P, decryptionResult) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy new file mode 100644 index 00000000..5fc12d23 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.AeadRequest +import io.jsonwebtoken.security.AeadResult +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +import static org.junit.Assert.assertArrayEquals + +/** + * Tests successful encryption and decryption using 'AES_256_CBC_HMAC_SHA_512' as defined in + * RFC 7518, Appendix B.3 + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7518AppendixB3Test { + + final byte[] K = + [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f] as byte[] + final SecretKey KEY = new SecretKeySpec(K, "AES") + + final byte[] P = + [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[] + + final byte[] IV = + [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[] + + final byte[] A = + [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[] + + @SuppressWarnings('unused') + final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[] + + final byte[] E = + [0x4a, 0xff, 0xaa, 0xad, 0xb7, 0x8c, 0x31, 0xc5, 0xda, 0x4b, 0x1b, 0x59, 0x0d, 0x10, 0xff, 0xbd, + 0x3d, 0xd8, 0xd5, 0xd3, 0x02, 0x42, 0x35, 0x26, 0x91, 0x2d, 0xa0, 0x37, 0xec, 0xbc, 0xc7, 0xbd, + 0x82, 0x2c, 0x30, 0x1d, 0xd6, 0x7c, 0x37, 0x3b, 0xcc, 0xb5, 0x84, 0xad, 0x3e, 0x92, 0x79, 0xc2, + 0xe6, 0xd1, 0x2a, 0x13, 0x74, 0xb7, 0x7f, 0x07, 0x75, 0x53, 0xdf, 0x82, 0x94, 0x10, 0x44, 0x6b, + 0x36, 0xeb, 0xd9, 0x70, 0x66, 0x29, 0x6a, 0xe6, 0x42, 0x7e, 0xa7, 0x5c, 0x2e, 0x08, 0x46, 0xa1, + 0x1a, 0x09, 0xcc, 0xf5, 0x37, 0x0d, 0xc8, 0x0b, 0xfe, 0xcb, 0xad, 0x28, 0xc7, 0x3f, 0x09, 0xb3, + 0xa3, 0xb7, 0x5e, 0x66, 0x2a, 0x25, 0x94, 0x41, 0x0a, 0xe4, 0x96, 0xb2, 0xe2, 0xe6, 0x60, 0x9e, + 0x31, 0xe6, 0xe0, 0x2c, 0xc8, 0x37, 0xf0, 0x53, 0xd2, 0x1f, 0x37, 0xff, 0x4f, 0x51, 0x95, 0x0b, + 0xbe, 0x26, 0x38, 0xd0, 0x9d, 0xd7, 0xa4, 0x93, 0x09, 0x30, 0x80, 0x6d, 0x07, 0x03, 0xb1, 0xf6] as byte[] + + @SuppressWarnings('unused') + final byte[] M = + [0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf, + 0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5, + 0xfd, 0x30, 0xa5, 0x65, 0xc6, 0x16, 0xff, 0xb2, 0xf3, 0x64, 0xba, 0xec, 0xe6, 0x8f, 0xc4, 0x07, + 0x53, 0xbc, 0xfc, 0x02, 0x5d, 0xde, 0x36, 0x93, 0x75, 0x4a, 0xa1, 0xf5, 0xc3, 0x37, 0x3b, 0x9c] as byte[] + + final byte[] T = + [0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf, + 0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5] as byte[] + + @Test + void test() { + + def alg = Jwts.ENC.A256CBC_HS512 + AeadRequest req = new DefaultAeadRequest(P, null, null, KEY, A, IV) + AeadResult result = alg.encrypt(req) + + byte[] resultCiphertext = result.getPayload() + byte[] resultTag = result.getDigest() + byte[] resultIv = result.getInitializationVector() + + assertArrayEquals E, resultCiphertext + assertArrayEquals T, resultTag + assertArrayEquals IV, resultIv //shouldn't have been altered + + // now test decryption: + def dreq = new DefaultAeadResult(null, null, resultCiphertext, KEY, A, resultTag, resultIv) + byte[] decryptionResult = alg.decrypt(dreq).getPayload() + assertArrayEquals(P, decryptionResult) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy new file mode 100644 index 00000000..202690f3 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -0,0 +1,136 @@ +/* + * 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.security + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwe +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Services +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.security.* +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.KeyPair +import java.security.spec.ECParameterSpec + +import static org.junit.Assert.* + +class RFC7518AppendixCTest { + + private static final String rfcString(String s) { + return s.replaceAll('[\\s]', '') + } + + private static final Map fromEncoded(String s) { + byte[] json = Decoders.BASE64URL.decode(s) + return Services.loadFirst(Deserializer.class).deserialize(json) as Map + } + + private static final Map fromJson(String s) { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8) + return Services.loadFirst(Deserializer.class).deserialize(bytes) as Map + } + + private static EcPrivateJwk readJwk(String json) { + Map m = fromJson(json) + return Jwks.builder().putAll(m).build() as EcPrivateJwk + } + + // https://www.rfc-editor.org/rfc/rfc7517.html#appendix-C.1 + private static final String ALICE_EPHEMERAL_JWK_STRING = rfcString(''' + {"kty":"EC", + "crv":"P-256", + "x":"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y":"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", + "d":"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" + }''') + + private static final String BOB_PRIVATE_JWK_STRING = rfcString(''' + {"kty":"EC", + "crv":"P-256", + "x":"weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", + "y":"e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", + "d":"VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" + }''') + + private static final String RFC_HEADER_JSON_STRING = rfcString(''' + {"alg":"ECDH-ES", + "enc":"A128GCM", + "apu":"QWxpY2U", + "apv":"Qm9i", + "epk": + {"kty":"EC", + "crv":"P-256", + "x":"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", + "y":"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps" + } + } + ''') + + private static final byte[] RFC_DERIVED_KEY = + [86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26] as byte[] + + @Test + void test() { + EcPrivateJwk aliceJwk = readJwk(ALICE_EPHEMERAL_JWK_STRING) + EcPrivateJwk bobJwk = readJwk(BOB_PRIVATE_JWK_STRING) + + Map RFC_HEADER = fromJson(RFC_HEADER_JSON_STRING) + + byte[] derivedKey = null + + def alg = new EcdhKeyAlgorithm() { + + //ensure keypair reflects required RFC test value: + @Override + protected KeyPair generateKeyPair(Request request, ECParameterSpec spec) { + return aliceJwk.toKeyPair().toJavaKeyPair() + } + + @Override + KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { + KeyResult result = super.getEncryptionKey(request) + // save result derived key so we can compare with the RFC value: + derivedKey = result.getKey().getEncoded() + return result + } + } + + String jwe = Jwts.builder() + .setHeader(Jwts.header().setAgreementPartyUInfo("Alice").setAgreementPartyVInfo("Bob")) + .claim("Hello", "World") + .encryptWith(bobJwk.toPublicJwk().toKey(), alg, Jwts.ENC.A128GCM) + .compact() + + // Ensure the protected header produced by JJWT is identical to the one in the RFC: + String encodedProtectedHeader = jwe.substring(0, jwe.indexOf('.')) + Map protectedHeader = fromEncoded(encodedProtectedHeader) + assertEquals RFC_HEADER, protectedHeader + + assertNotNull derivedKey + assertArrayEquals RFC_DERIVED_KEY, derivedKey + + // now reverse the process and ensure it all works: + Jwe claimsJwe = Jwts.parserBuilder() + .decryptWith(bobJwk.toKey()) + .build().parseClaimsJwe(jwe) + + assertEquals RFC_HEADER, claimsJwe.getHeader() + assertEquals "World", claimsJwe.getPayload().get("Hello") + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section3Test.groovy new file mode 100644 index 00000000..d0555e74 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section3Test.groovy @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +/** + * Tests successful parsing/creation of RFC 7520, Section 3 + * JSON Web Key Examples. + * + * @since JJWT_RELEASE_VERSION + */ +class RFC7520Section3Test { + + static final String FIGURE_2 = Strings.trimAllWhitespace(''' + { + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-521", + "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9 + A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", + "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVy + SsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", + "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zb + KipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt" + }''') + + static final String FIGURE_4 = Strings.trimAllWhitespace(''' + { + "kty": "RSA", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT + -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV + wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj- + oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde + 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC + LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g + HdrNP5zw", + "e": "AQAB", + "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78e + iZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRld + Y7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-b + MwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU + 6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDj + d18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOc + OpBrQzwQ", + "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nR + aO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmG + peNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8 + bUq0k", + "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT + 8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7an + V5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0 + s7pFc", + "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q + 1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn + -RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX + 59ehik", + "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pEr + AMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJK + bi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdK + T1cYF8", + "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-N + ZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDh + jJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpP + z8aaI4" + }''') + + static final String FIGURE_5 = Strings.trimAllWhitespace(''' + { + "kty": "oct", + "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", + "use": "sig", + "alg": "HS256", + "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" + } + ''') + + static final String FIGURE_6 = Strings.trimAllWhitespace(''' + { + "kty": "oct", + "kid": "1e571774-2e08-40da-8308-e8d68773842d", + "use": "enc", + "alg": "A256GCM", + "k": "AAPapAv4LbFbiVawEjagUBluYqN5rhna-8nuldDvOx8" + } + ''') + + @Test + void testSection3_1() { // EC Public Key + String jwkString = Strings.trimAllWhitespace(''' + { + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-521", + "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9 + A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", + "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVy + SsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1" + }''') + + EcPublicJwk jwk = Jwks.parser().build().parse(jwkString) as EcPublicJwk + assertEquals 'EC', jwk.getType() + assertEquals 'sig', jwk.getPublicKeyUse() + assertEquals 'bilbo.baggins@hobbiton.example', jwk.getId() + assertEquals 'P-521', jwk.get('crv') + assertTrue jwk.toKey() instanceof ECPublicKey + } + + @Test + void testSection3_2() { // EC Private Key + EcPrivateJwk jwk = Jwks.parser().build().parse(FIGURE_2) as EcPrivateJwk + assertEquals 'EC', jwk.getType() + assertEquals 'sig', jwk.getPublicKeyUse() + assertEquals 'bilbo.baggins@hobbiton.example', jwk.getId() + assertEquals 'P-521', jwk.get('crv') + assertTrue jwk.toKey() instanceof ECPrivateKey + } + + @Test + void testSection3_3() { // RSA Public Key + String s = Strings.trimAllWhitespace(''' + { + "kty": "RSA", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT + -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV + wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj- + oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde + 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC + LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g + HdrNP5zw", + "e": "AQAB" + }''') + + RsaPublicJwk jwk = Jwks.parser().build().parse(s) as RsaPublicJwk + assertEquals 'RSA', jwk.getType() + assertEquals 'sig', jwk.getPublicKeyUse() + assertEquals 'bilbo.baggins@hobbiton.example', jwk.getId() + assertEquals 256, Bytes.length(Decoders.BASE64URL.decode(jwk.get('n') as String)) + assertTrue jwk.toKey() instanceof RSAPublicKey + } + + @Test + void testSection3_4() { // RSA Private Key + RsaPrivateJwk jwk = Jwks.parser().build().parse(FIGURE_4) as RsaPrivateJwk + assertEquals 'RSA', jwk.getType() + assertEquals 'sig', jwk.getPublicKeyUse() + assertEquals 'bilbo.baggins@hobbiton.example', jwk.getId() + assertEquals 256, Bytes.length(Decoders.BASE64URL.decode(jwk.get('n') as String)) + assertTrue Bytes.length(Decoders.BASE64URL.decode(jwk.get('d') as String)) <= 256 + assertTrue jwk.toKey() instanceof RSAPrivateKey + } + + @Test + void testSection3_5() { // Symmetric Key (MAC) + SecretJwk jwk = Jwks.parser().build().parse(FIGURE_5) as SecretJwk + assertEquals 'oct', jwk.getType() + assertEquals '018c0ae5-4d9b-471b-bfd6-eef314bc7037', jwk.getId() + assertEquals 'sig', jwk.get('use') + assertEquals 'HS256', jwk.getAlgorithm() + SecretKey key = jwk.toKey() + assertEquals 256, Bytes.bitLength(key.getEncoded()) + } + + @Test + void testSection3_6() { // Symmetric Key (Encryption) + SecretJwk jwk = Jwks.parser().build().parse(FIGURE_6) as SecretJwk + assertEquals 'oct', jwk.getType() + assertEquals '1e571774-2e08-40da-8308-e8d68773842d', jwk.getId() + assertEquals 'enc', jwk.get('use') + assertEquals 'A256GCM', jwk.getAlgorithm() + SecretKey key = jwk.toKey() + assertEquals 256, Bytes.bitLength(key.getEncoded()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy new file mode 100644 index 00000000..05a7507a --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section4Test.groovy @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.io.SerializationException +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.EcPrivateJwk +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import io.jsonwebtoken.security.SecretJwk +import org.junit.Test + +import javax.crypto.SecretKey +import java.nio.charset.StandardCharsets +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.RSAPrivateKey + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +class RFC7520Section4Test { + + static final byte[] utf8(String s) { + return s.getBytes(StandardCharsets.UTF_8) + } + + static final String utf8(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8) + } + + static final String b64Url(byte[] bytes) { + return Encoders.BASE64URL.encode(bytes) + } + + static final byte[] b64Url(String s) { + return Decoders.BASE64URL.decode(s) + } + + static final String FIGURE_7 = + "It\u2019s a dangerous business, Frodo, going out your " + + "door. You step onto the road, and if you don't keep your feet, " + + "there\u2019s no knowing where you might be swept off " + + "to." + + static final String FIGURE_8 = Strings.trimAllWhitespace(''' + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4 + ''') + + static final String FIGURE_9 = Strings.trimAllWhitespace(''' + { + "alg": "RS256", + "kid": "bilbo.baggins@hobbiton.example" + } + ''') + + static final String FIGURE_10 = Strings.trimAllWhitespace(''' + eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX + hhbXBsZSJ9 + ''') + + static final String FIGURE_13 = Strings.trimAllWhitespace(''' + eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX + hhbXBsZSJ9 + . + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4 + . + MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK + ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J + IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w + W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP + xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f + cIe8u9ipH84ogoree7vjbU5y18kDquDg + ''') + + static final String FIGURE_16 = Strings.trimAllWhitespace(''' + { + "alg": "PS384", + "kid": "bilbo.baggins@hobbiton.example" + } + ''') + + static final String FIGURE_17 = Strings.trimAllWhitespace(''' + eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX + hhbXBsZSJ9 + ''') + + static final String FIGURE_20 = Strings.trimAllWhitespace(''' + eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX + hhbXBsZSJ9 + . + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4 + . + cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2I + pN6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXU + vdvWXzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRX + e8P_ijQ7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT + 0qI0n6uiP1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a + 6GYmJUAfmWjwZ6oD4ifKo8DYM-X72Eaw + ''') + + static final String FIGURE_23 = Strings.trimAllWhitespace(''' + { + "alg": "ES512", + "kid": "bilbo.baggins@hobbiton.example" + } + ''') + + static final String FIGURE_24 = Strings.trimAllWhitespace(''' + eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX + hhbXBsZSJ9 + ''') + + static final String FIGURE_27 = Strings.trimAllWhitespace(''' + eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX + hhbXBsZSJ9 + . + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4 + . + AE_R_YZCChjn4791jSQCrdPZCNYqHXCTZH0-JZGYNlaAjP2kqaluUIIUnC9qvb + u9Plon7KRTzoNEuT4Va2cmL1eJAQy3mtPBu_u_sDDyYjnAMDxXPn7XrT0lw-kv + AD890jl8e2puQens_IEKBpHABlsbEPX6sFY8OcGDqoRuBomu9xQ2 + ''') + + static final String FIGURE_30 = Strings.trimAllWhitespace(''' + { + "alg": "HS256", + "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037" + } + ''') + + static final String FIGURE_31 = Strings.trimAllWhitespace(''' + eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW + VlZjMxNGJjNzAzNyJ9 + ''') + + static final String FIGURE_34 = Strings.trimAllWhitespace(''' + eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW + VlZjMxNGJjNzAzNyJ9 + . + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4 + . + s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0 + ''') + + static final String FIGURE_37 = FIGURE_30 // same in RFC + static final String FIGURE_38 = FIGURE_31 // same in RFC + static final String FIGURE_41 = Strings.trimAllWhitespace(''' + eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW + VlZjMxNGJjNzAzNyJ9 + . + . + s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0 + ''') + + + static { + //ensure our representations match the RFC: + assert FIGURE_7.equals(utf8(b64Url(FIGURE_8))) + assert FIGURE_10.equals(b64Url(utf8(FIGURE_9))) + assert FIGURE_17.equals(b64Url(utf8(FIGURE_16))) + assert FIGURE_24.equals(b64Url(utf8(FIGURE_23))) + assert FIGURE_31.equals(b64Url(utf8(FIGURE_30))) + assert FIGURE_38.equals(b64Url(utf8(FIGURE_37))) + } + + @Test + void testSection4_1() { + + RsaPrivateJwk jwk = Jwks.parser().build().parse(RFC7520Section3Test.FIGURE_4) as RsaPrivateJwk + RSAPrivateKey key = jwk.toKey() + + def alg = Jwts.SIG.RS256 + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 2, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_9) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_7) + .signWith(key, alg) + .compact() + + assertEquals FIGURE_13, result + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().verifyWith(jwk.toPublicJwk().toKey()).build().parseContentJws(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals FIGURE_7, utf8(parsed.payload) + } + + @Test + void testSection4_2() { + + RsaPrivateJwk jwk = Jwks.parser().build().parse(RFC7520Section3Test.FIGURE_4) as RsaPrivateJwk + RSAPrivateKey key = jwk.toKey() + + def alg = Jwts.SIG.PS384 + String kid = 'bilbo.baggins@hobbiton.example' + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 2, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals kid, m.get('kid') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_16) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', kid) + .setPayload(FIGURE_7) + .signWith(key, alg) + .compact() + + + // As reminded in https://www.rfc-editor.org/rfc/rfc7520.html#section-4.2, it is not possible to + // generate the same exact signature because RSASSA-PSS uses random data during signature creation + // so we at least assert that our result starts with the RFC value, ignoring the final signature + assertTrue result.startsWith(FIGURE_20.substring(0, FIGURE_20.lastIndexOf('.'))) + + // even though we can't know what the signature output is ahead of time due to random data, we can assert + // the signature to guarantee a round trip works as expected: + def parsed = Jwts.parserBuilder() + .verifyWith(jwk.toPublicJwk().toKey()) + .build().parseContentJws(result) + + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals kid, parsed.header.getKeyId() + assertEquals FIGURE_7, utf8(parsed.payload) + } + + @Test + void testSection4_3() { + + EcPrivateJwk jwk = Jwks.parser().build().parse(RFC7520Section3Test.FIGURE_2) as EcPrivateJwk + ECPrivateKey key = jwk.toKey() + + def alg = Jwts.SIG.ES512 + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 2, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_23) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_7) + .signWith(key, alg) + .compact() + + // As reminded in https://www.rfc-editor.org/rfc/rfc7520.html#section-4.3, it is not possible to + // generate the same exact signature because RSASSA-PSS uses random data during signature creation + // so we at least assert that our result starts with the RFC value, ignoring the final signature + assertTrue result.startsWith(FIGURE_27.substring(0, FIGURE_27.lastIndexOf('.'))) + + // even though we can't know what the signature output is ahead of time due to random data, we can assert + // the signature to guarantee a round trip works as expected: + def parsed = Jwts.parserBuilder() + .verifyWith(jwk.toPublicJwk().toKey()) + .build().parseContentJws(result) + + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals FIGURE_7, utf8(parsed.payload) + } + + @Test + void testSection4_4() { + + SecretJwk jwk = Jwks.parser().build().parse(RFC7520Section3Test.FIGURE_5) as SecretJwk + SecretKey key = jwk.toKey() + + def alg = Jwts.SIG.HS256 + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 2, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_30) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_7) + .signWith(key, alg) + .compact() + + assertEquals FIGURE_34, result + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().verifyWith(key).build().parseContentJws(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals FIGURE_7, utf8(parsed.payload) + } + + @Test + void testSection4_5() { + + SecretJwk jwk = Jwks.parser().build().parse(RFC7520Section3Test.FIGURE_5) as SecretJwk + SecretKey key = jwk.toKey() + + def alg = Jwts.SIG.HS256 + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 2, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_37) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_7) + .signWith(key, alg) + .compact() + + String detached = result.substring(0, result.indexOf('.')) + '..' + + result.substring(result.lastIndexOf('.') + 1, result.length()) + + assertEquals FIGURE_41, detached + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().verifyWith(key).build().parseContentJws(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals FIGURE_7, utf8(parsed.payload) + } + + // void testSection4_6() {} we don't support non-compact JSON serialization yet + // void testSection4_7() {} we don't support non-compact JSON serialization yet + // void testSection4_8() {} we don't support non-compact JSON serialization yet +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy new file mode 100644 index 00000000..0a3097fe --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7520Section5Test.groovy @@ -0,0 +1,679 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.io.SerializationException +import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.* +import org.junit.Test + +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.Key +import java.security.KeyPair +import java.security.interfaces.RSAPublicKey +import java.security.spec.ECParameterSpec + +import static org.junit.Assert.assertEquals + +class RFC7520Section5Test { + + static final byte[] utf8(String s) { + return s.getBytes(StandardCharsets.UTF_8) + } + + static final String utf8(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8) + } + + static final String b64Url(byte[] bytes) { + return Encoders.BASE64URL.encode(bytes) + } + + static final byte[] b64Url(String s) { + return Decoders.BASE64URL.decode(s) + } + + static final String FIGURE_72 = + "You can trust us to stick with you through thick and " + + "thin\u2013to the bitter end. And you can trust us to " + + "keep any secret of yours\u2013closer than you keep it " + + "yourself. But you cannot trust us to let you face trouble " + + "alone, and go off without a word. We are your friends, Frodo." + + static final String FIGURE_73 = Strings.trimAllWhitespace(''' + { + "kty": "RSA", + "kid": "frodo.baggins@hobbiton.example", + "use": "enc", + "n": "maxhbsmBtdQ3CNrKvprUE6n9lYcregDMLYNeTAWcLj8NnPU9XIYegT + HVHQjxKDSHP2l-F5jS7sppG1wgdAqZyhnWvXhYNvcM7RfgKxqNx_xAHx + 6f3yy7s-M9PSNCwPC2lh6UAkR4I00EhV9lrypM9Pi4lBUop9t5fS9W5U + NwaAllhrd-osQGPjIeI1deHTwx-ZTHu3C60Pu_LJIl6hKn9wbwaUmA4c + R5Bd2pgbaY7ASgsjCUbtYJaNIHSoHXprUdJZKUMAzV0WOKPfA6OPI4oy + pBadjvMZ4ZAj3BnXaSYsEZhaueTXvZB4eZOAjIyh2e_VOIKVMsnDrJYA + VotGlvMQ", + "e": "AQAB", + "d": "Kn9tgoHfiTVi8uPu5b9TnwyHwG5dK6RE0uFdlpCGnJN7ZEi963R7wy + bQ1PLAHmpIbNTztfrheoAniRV1NCIqXaW_qS461xiDTp4ntEPnqcKsyO + 5jMAji7-CL8vhpYYowNFvIesgMoVaPRYMYT9TW63hNM0aWs7USZ_hLg6 + Oe1mY0vHTI3FucjSM86Nff4oIENt43r2fspgEPGRrdE6fpLc9Oaq-qeP + 1GFULimrRdndm-P8q8kvN3KHlNAtEgrQAgTTgz80S-3VD0FgWfgnb1PN + miuPUxO8OpI9KDIfu_acc6fg14nsNaJqXe6RESvhGPH2afjHqSy_Fd2v + pzj85bQQ", + "p": "2DwQmZ43FoTnQ8IkUj3BmKRf5Eh2mizZA5xEJ2MinUE3sdTYKSLtaE + oekX9vbBZuWxHdVhM6UnKCJ_2iNk8Z0ayLYHL0_G21aXf9-unynEpUsH + 7HHTklLpYAzOOx1ZgVljoxAdWNn3hiEFrjZLZGS7lOH-a3QQlDDQoJOJ + 2VFmU", + "q": "te8LY4-W7IyaqH1ExujjMqkTAlTeRbv0VLQnfLY2xINnrWdwiQ93_V + F099aP1ESeLja2nw-6iKIe-qT7mtCPozKfVtUYfz5HrJ_XY2kfexJINb + 9lhZHMv5p1skZpeIS-GPHCC6gRlKo1q-idn_qxyusfWv7WAxlSVfQfk8 + d6Et0", + "dp": "UfYKcL_or492vVc0PzwLSplbg4L3-Z5wL48mwiswbpzOyIgd2xHTH + QmjJpFAIZ8q-zf9RmgJXkDrFs9rkdxPtAsL1WYdeCT5c125Fkdg317JV + RDo1inX7x2Kdh8ERCreW8_4zXItuTl_KiXZNU5lvMQjWbIw2eTx1lpsf + lo0rYU", + "dq": "iEgcO-QfpepdH8FWd7mUFyrXdnOkXJBCogChY6YKuIHGc_p8Le9Mb + pFKESzEaLlN1Ehf3B6oGBl5Iz_ayUlZj2IoQZ82znoUrpa9fVYNot87A + CfzIG7q9Mv7RiPAderZi03tkVXAdaBau_9vs5rS-7HMtxkVrxSUvJY14 + TkXlHE", + "qi": "kC-lzZOqoFaZCr5l0tOVtREKoVqaAYhQiqIRGL-MzS4sCmRkxm5vZ + lXYx6RtE1n_AagjqajlkjieGlxTTThHD8Iga6foGBMaAr5uR1hGQpSc7 + Gl7CF1DZkBJMTQN6EshYzZfxW08mIO8M6Rzuh0beL6fG9mkDcIyPrBXx + 2bQ_mM" + } + ''') + + static final String FIGURE_74 = '3qyTVhIWt5juqZUCpfRqpvauwB956MEJL2Rt-8qXKSo' + static final String FIGURE_75 = 'bbd5sTkYwhAIqfHsx8DayA' + static final String FIGURE_76 = Strings.trimAllWhitespace(''' + laLxI0j-nLH-_BgLOXMozKxmy9gffy2gTdvqzfTihJBuuzxg0V7yk1WClnQePF + vG2K-pvSlWc9BRIazDrn50RcRai__3TDON395H3c62tIouJJ4XaRvYHFjZTZ2G + Xfz8YAImcc91Tfk0WXC2F5Xbb71ClQ1DDH151tlpH77f2ff7xiSxh9oSewYrcG + TSLUeeCt36r1Kt3OSj7EyBQXoZlN7IxbyhMAfgIe7Mv1rOTOI5I8NQqeXXW8Vl + zNmoxaGMny3YnGir5Wf6Qt2nBq4qDaPdnaAuuGUGEecelIO1wx1BpyIfgvfjOh + MBs9M8XL223Fg47xlGsMXdfuY-4jaqVw + ''') + + static final String FIGURE_77 = Strings.trimAllWhitespace(''' + { + "alg": "RSA1_5", + "kid": "frodo.baggins@hobbiton.example", + "enc": "A128CBC-HS256" + } + ''') + static final String FIGURE_78 = Strings.trimAllWhitespace(''' + eyJhbGciOiJSU0ExXzUiLCJraWQiOiJmcm9kby5iYWdnaW5zQGhvYmJpdG9uLm + V4YW1wbGUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0 + ''') + + static final String FIGURE_81 = Strings.trimAllWhitespace(''' + eyJhbGciOiJSU0ExXzUiLCJraWQiOiJmcm9kby5iYWdnaW5zQGhvYmJpdG9uLm + V4YW1wbGUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0 + . + laLxI0j-nLH-_BgLOXMozKxmy9gffy2gTdvqzfTihJBuuzxg0V7yk1WClnQePF + vG2K-pvSlWc9BRIazDrn50RcRai__3TDON395H3c62tIouJJ4XaRvYHFjZTZ2G + Xfz8YAImcc91Tfk0WXC2F5Xbb71ClQ1DDH151tlpH77f2ff7xiSxh9oSewYrcG + TSLUeeCt36r1Kt3OSj7EyBQXoZlN7IxbyhMAfgIe7Mv1rOTOI5I8NQqeXXW8Vl + zNmoxaGMny3YnGir5Wf6Qt2nBq4qDaPdnaAuuGUGEecelIO1wx1BpyIfgvfjOh + MBs9M8XL223Fg47xlGsMXdfuY-4jaqVw + . + bbd5sTkYwhAIqfHsx8DayA + . + 0fys_TY_na7f8dwSfXLiYdHaA2DxUjD67ieF7fcVbIR62JhJvGZ4_FNVSiGc_r + aa0HnLQ6s1P2sv3Xzl1p1l_o5wR_RsSzrS8Z-wnI3Jvo0mkpEEnlDmZvDu_k8O + WzJv7eZVEqiWKdyVzFhPpiyQU28GLOpRc2VbVbK4dQKPdNTjPPEmRqcaGeTWZV + yeSUvf5k59yJZxRuSvWFf6KrNtmRdZ8R4mDOjHSrM_s8uwIFcqt4r5GX8TKaI0 + zT5CbL5Qlw3sRc7u_hg0yKVOiRytEAEs3vZkcfLkP6nbXdC_PkMdNS-ohP78T2 + O6_7uInMGhFeX4ctHG7VelHGiT93JfWDEQi5_V9UN1rhXNrYu-0fVMkZAKX3VW + i7lzA6BP430m + . + kvKuFBXHe5mQr4lqgobAUg + ''') + + static final String FIGURE_84 = Strings.trimAllWhitespace(''' + { + "kty": "RSA", + "kid": "samwise.gamgee@hobbiton.example", + "use": "enc", + "n": "wbdxI55VaanZXPY29Lg5hdmv2XhvqAhoxUkanfzf2-5zVUxa6prHRr + I4pP1AhoqJRlZfYtWWd5mmHRG2pAHIlh0ySJ9wi0BioZBl1XP2e-C-Fy + XJGcTy0HdKQWlrfhTm42EW7Vv04r4gfao6uxjLGwfpGrZLarohiWCPnk + Nrg71S2CuNZSQBIPGjXfkmIy2tl_VWgGnL22GplyXj5YlBLdxXp3XeSt + sqo571utNfoUTU8E4qdzJ3U1DItoVkPGsMwlmmnJiwA7sXRItBCivR4M + 5qnZtdw-7v4WuR4779ubDuJ5nalMv2S66-RPcnFAzWSKxtBDnFJJDGIU + e7Tzizjg1nms0Xq_yPub_UOlWn0ec85FCft1hACpWG8schrOBeNqHBOD + FskYpUc2LC5JA2TaPF2dA67dg1TTsC_FupfQ2kNGcE1LgprxKHcVWYQb + 86B-HozjHZcqtauBzFNV5tbTuB-TpkcvJfNcFLlH3b8mb-H_ox35FjqB + SAjLKyoeqfKTpVjvXhd09knwgJf6VKq6UC418_TOljMVfFTWXUxlnfhO + OnzW6HSSzD1c9WrCuVzsUMv54szidQ9wf1cYWf3g5qFDxDQKis99gcDa + iCAwM3yEBIzuNeeCa5dartHDb1xEB_HcHSeYbghbMjGfasvKn0aZRsnT + yC0xhWBlsolZE", + "e": "AQAB", + "alg": "RSA-OAEP", + "d": "n7fzJc3_WG59VEOBTkayzuSMM780OJQuZjN_KbH8lOZG25ZoA7T4Bx + cc0xQn5oZE5uSCIwg91oCt0JvxPcpmqzaJZg1nirjcWZ-oBtVk7gCAWq + -B3qhfF3izlbkosrzjHajIcY33HBhsy4_WerrXg4MDNE4HYojy68TcxT + 2LYQRxUOCf5TtJXvM8olexlSGtVnQnDRutxEUCwiewfmmrfveEogLx9E + A-KMgAjTiISXxqIXQhWUQX1G7v_mV_Hr2YuImYcNcHkRvp9E7ook0876 + DhkO8v4UOZLwA1OlUX98mkoqwc58A_Y2lBYbVx1_s5lpPsEqbbH-nqIj + h1fL0gdNfihLxnclWtW7pCztLnImZAyeCWAG7ZIfv-Rn9fLIv9jZ6r7r + -MSH9sqbuziHN2grGjD_jfRluMHa0l84fFKl6bcqN1JWxPVhzNZo01yD + F-1LiQnqUYSepPf6X3a2SOdkqBRiquE6EvLuSYIDpJq3jDIsgoL8Mo1L + oomgiJxUwL_GWEOGu28gplyzm-9Q0U0nyhEf1uhSR8aJAQWAiFImWH5W + _IQT9I7-yrindr_2fWQ_i1UgMsGzA7aOGzZfPljRy6z-tY_KuBG00-28 + S_aWvjyUc-Alp8AUyKjBZ-7CWH32fGWK48j1t-zomrwjL_mnhsPbGs0c + 9WsWgRzI-K8gE", + "p": "7_2v3OQZzlPFcHyYfLABQ3XP85Es4hCdwCkbDeltaUXgVy9l9etKgh + vM4hRkOvbb01kYVuLFmxIkCDtpi-zLCYAdXKrAK3PtSbtzld_XZ9nlsY + a_QZWpXB_IrtFjVfdKUdMz94pHUhFGFj7nr6NNxfpiHSHWFE1zD_AC3m + Y46J961Y2LRnreVwAGNw53p07Db8yD_92pDa97vqcZOdgtybH9q6uma- + RFNhO1AoiJhYZj69hjmMRXx-x56HO9cnXNbmzNSCFCKnQmn4GQLmRj9s + fbZRqL94bbtE4_e0Zrpo8RNo8vxRLqQNwIy85fc6BRgBJomt8QdQvIgP + gWCv5HoQ", + "q": "zqOHk1P6WN_rHuM7ZF1cXH0x6RuOHq67WuHiSknqQeefGBA9PWs6Zy + KQCO-O6mKXtcgE8_Q_hA2kMRcKOcvHil1hqMCNSXlflM7WPRPZu2qCDc + qssd_uMbP-DqYthH_EzwL9KnYoH7JQFxxmcv5An8oXUtTwk4knKjkIYG + RuUwfQTus0w1NfjFAyxOOiAQ37ussIcE6C6ZSsM3n41UlbJ7TCqewzVJ + aPJN5cxjySPZPD3Vp01a9YgAD6a3IIaKJdIxJS1ImnfPevSJQBE79-EX + e2kSwVgOzvt-gsmM29QQ8veHy4uAqca5dZzMs7hkkHtw1z0jHV90epQJ + JlXXnH8Q", + "dp": "19oDkBh1AXelMIxQFm2zZTqUhAzCIr4xNIGEPNoDt1jK83_FJA-xn + x5kA7-1erdHdms_Ef67HsONNv5A60JaR7w8LHnDiBGnjdaUmmuO8XAxQ + J_ia5mxjxNjS6E2yD44USo2JmHvzeeNczq25elqbTPLhUpGo1IZuG72F + ZQ5gTjXoTXC2-xtCDEUZfaUNh4IeAipfLugbpe0JAFlFfrTDAMUFpC3i + XjxqzbEanflwPvj6V9iDSgjj8SozSM0dLtxvu0LIeIQAeEgT_yXcrKGm + pKdSO08kLBx8VUjkbv_3Pn20Gyu2YEuwpFlM_H1NikuxJNKFGmnAq9Lc + nwwT0jvoQ", + "dq": "S6p59KrlmzGzaQYQM3o0XfHCGvfqHLYjCO557HYQf72O9kLMCfd_1 + VBEqeD-1jjwELKDjck8kOBl5UvohK1oDfSP1DleAy-cnmL29DqWmhgwM + 1ip0CCNmkmsmDSlqkUXDi6sAaZuntyukyflI-qSQ3C_BafPyFaKrt1fg + dyEwYa08pESKwwWisy7KnmoUvaJ3SaHmohFS78TJ25cfc10wZ9hQNOrI + ChZlkiOdFCtxDqdmCqNacnhgE3bZQjGp3n83ODSz9zwJcSUvODlXBPc2 + AycH6Ci5yjbxt4Ppox_5pjm6xnQkiPgj01GpsUssMmBN7iHVsrE7N2iz + nBNCeOUIQ", + "qi": "FZhClBMywVVjnuUud-05qd5CYU0dK79akAgy9oX6RX6I3IIIPckCc + iRrokxglZn-omAY5CnCe4KdrnjFOT5YUZE7G_Pg44XgCXaarLQf4hl80 + oPEf6-jJ5Iy6wPRx7G2e8qLxnh9cOdf-kRqgOS3F48Ucvw3ma5V6KGMw + QqWFeV31XtZ8l5cVI-I3NzBS7qltpUVgz2Ju021eyc7IlqgzR98qKONl + 27DuEES0aK0WE97jnsyO27Yp88Wa2RiBrEocM89QZI1seJiGDizHRUP4 + UZxw9zsXww46wy0P6f9grnYp7t8LkyDDk8eoI4KX6SNMNVcyVS9IWjlq + 8EzqZEKIA" + } + ''') + static final String FIGURE_85 = 'mYMfsggkTAm0TbvtlFh2hyoXnbEzJQjMxmgLN3d8xXA' + static final String FIGURE_86 = '-nBoKLH0YkLZPSI9' + static final String FIGURE_87 = Strings.trimAllWhitespace(''' + rT99rwrBTbTI7IJM8fU3Eli7226HEB7IchCxNuh7lCiud48LxeolRdtFF4nzQi + beYOl5S_PJsAXZwSXtDePz9hk-BbtsTBqC2UsPOdwjC9NhNupNNu9uHIVftDyu + cvI6hvALeZ6OGnhNV4v1zx2k7O1D89mAzfw-_kT3tkuorpDU-CpBENfIHX1Q58 + -Aad3FzMuo3Fn9buEP2yXakLXYa15BUXQsupM4A1GD4_H4Bd7V3u9h8Gkg8Bpx + KdUV9ScfJQTcYm6eJEBz3aSwIaK4T3-dwWpuBOhROQXBosJzS1asnuHtVMt2pK + IIfux5BC6huIvmY7kzV7W7aIUrpYm_3H4zYvyMeq5pGqFmW2k8zpO878TRlZx7 + pZfPYDSXZyS0CfKKkMozT_qiCwZTSz4duYnt8hS4Z9sGthXn9uDqd6wycMagnQ + fOTs_lycTWmY-aqWVDKhjYNRf03NiwRtb5BE-tOdFwCASQj3uuAgPGrO2AWBe3 + 8UjQb0lvXn1SpyvYZ3WFc7WOJYaTa7A8DRn6MC6T-xDmMuxC0G7S2rscw5lQQU + 06MvZTlFOt0UvfuKBa03cxA_nIBIhLMjY2kOTxQMmpDPTr6Cbo8aKaOnx6ASE5 + Jx9paBpnNmOOKH35j_QlrQhDWUN6A2Gg8iFayJ69xDEdHAVCGRzN3woEI2ozDR + s + ''') + static final String FIGURE_88 = Strings.trimAllWhitespace(''' + { + "alg": "RSA-OAEP", + "kid": "samwise.gamgee@hobbiton.example", + "enc": "A256GCM" + }''') + static final String FIGURE_89 = Strings.trimAllWhitespace(''' + eyJhbGciOiJSU0EtT0FFUCIsImtpZCI6InNhbXdpc2UuZ2FtZ2VlQGhvYmJpdG + 9uLmV4YW1wbGUiLCJlbmMiOiJBMjU2R0NNIn0 + ''') + static final String FIGURE_92 = Strings.trimAllWhitespace(''' + eyJhbGciOiJSU0EtT0FFUCIsImtpZCI6InNhbXdpc2UuZ2FtZ2VlQGhvYmJpdG + 9uLmV4YW1wbGUiLCJlbmMiOiJBMjU2R0NNIn0 + . + rT99rwrBTbTI7IJM8fU3Eli7226HEB7IchCxNuh7lCiud48LxeolRdtFF4nzQi + beYOl5S_PJsAXZwSXtDePz9hk-BbtsTBqC2UsPOdwjC9NhNupNNu9uHIVftDyu + cvI6hvALeZ6OGnhNV4v1zx2k7O1D89mAzfw-_kT3tkuorpDU-CpBENfIHX1Q58 + -Aad3FzMuo3Fn9buEP2yXakLXYa15BUXQsupM4A1GD4_H4Bd7V3u9h8Gkg8Bpx + KdUV9ScfJQTcYm6eJEBz3aSwIaK4T3-dwWpuBOhROQXBosJzS1asnuHtVMt2pK + IIfux5BC6huIvmY7kzV7W7aIUrpYm_3H4zYvyMeq5pGqFmW2k8zpO878TRlZx7 + pZfPYDSXZyS0CfKKkMozT_qiCwZTSz4duYnt8hS4Z9sGthXn9uDqd6wycMagnQ + fOTs_lycTWmY-aqWVDKhjYNRf03NiwRtb5BE-tOdFwCASQj3uuAgPGrO2AWBe3 + 8UjQb0lvXn1SpyvYZ3WFc7WOJYaTa7A8DRn6MC6T-xDmMuxC0G7S2rscw5lQQU + 06MvZTlFOt0UvfuKBa03cxA_nIBIhLMjY2kOTxQMmpDPTr6Cbo8aKaOnx6ASE5 + Jx9paBpnNmOOKH35j_QlrQhDWUN6A2Gg8iFayJ69xDEdHAVCGRzN3woEI2ozDR + s + . + -nBoKLH0YkLZPSI9 + . + o4k2cnGN8rSSw3IDo1YuySkqeS_t2m1GXklSgqBdpACm6UJuJowOHC5ytjqYgR + L-I-soPlwqMUf4UgRWWeaOGNw6vGW-xyM01lTYxrXfVzIIaRdhYtEMRBvBWbEw + P7ua1DRfvaOjgZv6Ifa3brcAM64d8p5lhhNcizPersuhw5f-pGYzseva-TUaL8 + iWnctc-sSwy7SQmRkfhDjwbz0fz6kFovEgj64X1I5s7E6GLp5fnbYGLa1QUiML + 7Cc2GxgvI7zqWo0YIEc7aCflLG1-8BboVWFdZKLK9vNoycrYHumwzKluLWEbSV + maPpOslY2n525DxDfWaVFUfKQxMF56vn4B9QMpWAbnypNimbM8zVOw + . + UCGiqJxhBI3IFVdPalHHvA + ''') + + static final String FIGURE_95 = Strings.trimAllWhitespace(''' + { + "keys": [ + { + "kty": "oct", + "kid": "77c7e2b8-6e13-45cf-8672-617b5b45243a", + "use": "enc", + "alg": "A128GCM", + "k": "XctOhJAkA-pD9Lh7ZgW_2A" + }, + { + "kty": "oct", + "kid": "81b20965-8332-43d9-a468-82160ad91ac8", + "use": "enc", + "alg": "A128KW", + "k": "GZy6sIZ6wl9NJOKB-jnmVQ" + }, + { + "kty": "oct", + "kid": "18ec08e1-bfa9-4d95-b205-2b4dd1d4321d", + "use": "enc", + "alg": "A256GCMKW", + "k": "qC57l_uxcm7Nm3K-ct4GFjx8tM1U8CZ0NLBvdQstiS8" + } + ] + } + ''') + static final String FIGURE_96 = 'entrap_o\u2013peter_long\u2013credit_tun' + static final String FIGURE_97 = 'uwsjJXaBK407Qaf0_zpcpmr1Cs0CC50hIUEyGNEt3m0' + static final String FIGURE_98 = 'VBiCzVHNoLiR3F4V82uoTQ' + static final String FIGURE_99 = '8Q1SzinasR3xchYz6ZZcHA' + static final String FIGURE_101 = Strings.trimAllWhitespace(''' + { + "alg": "PBES2-HS512+A256KW", + "p2s": "8Q1SzinasR3xchYz6ZZcHA", + "p2c": 8192, + "cty": "jwk-set+json", + "enc": "A128CBC-HS256" + } + ''') + static final String FIGURE_102 = Strings.trimAllWhitespace(''' + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJwMnMiOiI4UTFTemluYXNSM3 + hjaFl6NlpaY0hBIiwicDJjIjo4MTkyLCJjdHkiOiJqd2stc2V0K2pzb24iLCJl + bmMiOiJBMTI4Q0JDLUhTMjU2In0 + ''') + static final String FIGURE_105 = Strings.trimAllWhitespace(''' + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJwMnMiOiI4UTFTemluYXNSM3 + hjaFl6NlpaY0hBIiwicDJjIjo4MTkyLCJjdHkiOiJqd2stc2V0K2pzb24iLCJl + bmMiOiJBMTI4Q0JDLUhTMjU2In0 + . + d3qNhUWfqheyPp4H8sjOWsDYajoej4c5Je6rlUtFPWdgtURtmeDV1g + . + VBiCzVHNoLiR3F4V82uoTQ + . + 23i-Tb1AV4n0WKVSSgcQrdg6GRqsUKxjruHXYsTHAJLZ2nsnGIX86vMXqIi6IR + sfywCRFzLxEcZBRnTvG3nhzPk0GDD7FMyXhUHpDjEYCNA_XOmzg8yZR9oyjo6l + TF6si4q9FZ2EhzgFQCLO_6h5EVg3vR75_hkBsnuoqoM3dwejXBtIodN84PeqMb + 6asmas_dpSsz7H10fC5ni9xIz424givB1YLldF6exVmL93R3fOoOJbmk2GBQZL + _SEGllv2cQsBgeprARsaQ7Bq99tT80coH8ItBjgV08AtzXFFsx9qKvC982KLKd + PQMTlVJKkqtV4Ru5LEVpBZXBnZrtViSOgyg6AiuwaS-rCrcD_ePOGSuxvgtrok + AKYPqmXUeRdjFJwafkYEkiuDCV9vWGAi1DH2xTafhJwcmywIyzi4BqRpmdn_N- + zl5tuJYyuvKhjKv6ihbsV_k1hJGPGAxJ6wUpmwC4PTQ2izEm0TuSE8oMKdTw8V + 3kobXZ77ulMwDs4p + . + 0HlwodAhOCILG5SQ2LQ9dg + ''') + + static final String FIGURE_108 = Strings.trimAllWhitespace(''' + { + "kty": "EC", + "kid": "peregrin.took@tuckborough.example", + "use": "enc", + "crv": "P-384", + "x": "YU4rRUzdmVqmRtWOs2OpDE_T5fsNIodcG8G5FWPrTPMyxpzsSOGaQL + pe2FpxBmu2", + "y": "A8-yxCHxkfBz3hKZfI1jUYMjUhsEveZ9THuwFjH2sCNdtksRJU7D5- + SkgaFL1ETP", + "d": "iTx2pk7wW-GqJkHcEkFQb2EFyYcO7RugmaW3mRrQVAOUiPommT0Idn + YK2xDlZh-j" + } + ''') + static final String FIGURE_109 = 'Nou2ueKlP70ZXDbq9UrRwg' + static final String FIGURE_110 = 'mH-G2zVqgztUtnW_' + static final String FIGURE_111 = Strings.trimAllWhitespace(''' + { + "kty": "EC", + "crv": "P-384", + "x": "uBo4kHPw6kbjx5l0xowrd_oYzBmaz-GKFZu4xAFFkbYiWgutEK6iuE + DsQ6wNdNg3", + "y": "sp3p5SGhZVC2faXumI-e9JU2Mo8KpoYrFDr5yPNVtW4PgEwZOyQTA- + JdaY8tb7E0", + "d": "D5H4Y_5PSKZvhfVFbcCYJOtcGZygRgfZkpsBr59Icmmhe9sW6nkZ8W + fwhinUfWJg" + } + ''') + public static final String FIGURE_113 = Strings.trimAllWhitespace(''' + { + "alg": "ECDH-ES+A128KW", + "kid": "peregrin.took@tuckborough.example", + "epk": { + "kty": "EC", + "crv": "P-384", + "x": "uBo4kHPw6kbjx5l0xowrd_oYzBmaz-GKFZu4xAFFkbYiWgutEK6i + uEDsQ6wNdNg3", + "y": "sp3p5SGhZVC2faXumI-e9JU2Mo8KpoYrFDr5yPNVtW4PgEwZOyQT + A-JdaY8tb7E0" + }, + "enc": "A128GCM" + } + ''') + static final String FIGURE_114 = Strings.trimAllWhitespace(''' + eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImtpZCI6InBlcmVncmluLnRvb2tAdH + Vja2Jvcm91Z2guZXhhbXBsZSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAt + Mzg0IiwieCI6InVCbzRrSFB3Nmtiang1bDB4b3dyZF9vWXpCbWF6LUdLRlp1NH + hBRkZrYllpV2d1dEVLNml1RURzUTZ3TmROZzMiLCJ5Ijoic3AzcDVTR2haVkMy + ZmFYdW1JLWU5SlUyTW84S3BvWXJGRHI1eVBOVnRXNFBnRXdaT3lRVEEtSmRhWT + h0YjdFMCJ9LCJlbmMiOiJBMTI4R0NNIn0 + ''') + static final String FIGURE_117 = Strings.trimAllWhitespace(''' + eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImtpZCI6InBlcmVncmluLnRvb2tAdH + Vja2Jvcm91Z2guZXhhbXBsZSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAt + Mzg0IiwieCI6InVCbzRrSFB3Nmtiang1bDB4b3dyZF9vWXpCbWF6LUdLRlp1NH + hBRkZrYllpV2d1dEVLNml1RURzUTZ3TmROZzMiLCJ5Ijoic3AzcDVTR2haVkMy + ZmFYdW1JLWU5SlUyTW84S3BvWXJGRHI1eVBOVnRXNFBnRXdaT3lRVEEtSmRhWT + h0YjdFMCJ9LCJlbmMiOiJBMTI4R0NNIn0 + . + 0DJjBXri_kBcC46IkU5_Jk9BqaQeHdv2 + . + mH-G2zVqgztUtnW_ + . + tkZuOO9h95OgHJmkkrfLBisku8rGf6nzVxhRM3sVOhXgz5NJ76oID7lpnAi_cP + WJRCjSpAaUZ5dOR3Spy7QuEkmKx8-3RCMhSYMzsXaEwDdXta9Mn5B7cCBoJKB0 + IgEnj_qfo1hIi-uEkUpOZ8aLTZGHfpl05jMwbKkTe2yK3mjF6SBAsgicQDVCkc + Y9BLluzx1RmC3ORXaM0JaHPB93YcdSDGgpgBWMVrNU1ErkjcMqMoT_wtCex3w0 + 3XdLkjXIuEr2hWgeP-nkUZTPU9EoGSPj6fAS-bSz87RCPrxZdj_iVyC6QWcqAu + 07WNhjzJEPc4jVntRJ6K53NgPQ5p99l3Z408OUqj4ioYezbS6vTPlQ + . + WuGzxmcreYjpHGJoa17EBg + ''') + + static { + //ensure our representations match the RFC: + assert FIGURE_77.equals(utf8(b64Url(FIGURE_78))) + assert FIGURE_88.equals(utf8(b64Url(FIGURE_89))) + assert FIGURE_101.equals(utf8(b64Url(FIGURE_102))) + assert FIGURE_113.equals(utf8(b64Url(FIGURE_114))) + } + + // https://www.rfc-editor.org/rfc/rfc7520.html#section-5.1 + @Test + void testSection5_1() { + + RsaPrivateJwk jwk = Jwks.parser().build().parse(FIGURE_73) as RsaPrivateJwk + RSAPublicKey key = jwk.toPublicJwk().toKey() + + def alg = new DefaultRsaKeyAlgorithm(StandardKeyAlgorithmsBridge.RSA1_5_ID, StandardKeyAlgorithmsBridge.RSA1_5_TRANSFORMATION) { + @Override + SecretKey generateKey(KeyRequest request) { + byte[] encoded = b64Url(FIGURE_74) // ensure RFC required value + return new SecretKeySpec(encoded, "AES") + } + + @Override + protected JcaTemplate jca(Request request) { + return new JcaTemplate(getJcaName(), null) { + // overrides parent, Groovy doesn't pick it up due to generics signature: + @SuppressWarnings('unused') + byte[] withCipher(CheckedFunction fn) throws SecurityException { + return b64Url(FIGURE_76) + } + } + } + } + + def enc = new HmacAesAeadAlgorithm(128) { + @Override + protected byte[] ensureInitializationVector(Request request) { + return b64Url(FIGURE_75) // ensure RFC required value + } + } + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 3, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + assertEquals enc.getId(), m.get('enc') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_77) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_72) + .encryptWith(key, alg, enc) + .compact() + + assertEquals FIGURE_81, result + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().decryptWith(jwk.toKey()).build().parseContentJwe(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals enc.getId(), parsed.header.getEncryptionAlgorithm() + assertEquals FIGURE_72, utf8(parsed.payload) + } + + @Test + void testSection5_2() { + + RsaPrivateJwk jwk = Jwks.parser().build().parse(FIGURE_84) as RsaPrivateJwk + RSAPublicKey key = jwk.toPublicJwk().toKey() + + def alg = new DefaultRsaKeyAlgorithm(StandardKeyAlgorithmsBridge.RSA_OAEP_ID, StandardKeyAlgorithmsBridge.RSA_OAEP_TRANSFORMATION) { + @Override + SecretKey generateKey(KeyRequest request) { + byte[] encoded = b64Url(FIGURE_85) // ensure RFC required value + return new SecretKeySpec(encoded, "AES") + } + + @Override + protected JcaTemplate jca(Request request) { + return new JcaTemplate(getJcaName(), null) { + // overrides parent, Groovy doesn't pick it up due to generics signature: + @SuppressWarnings('unused') + byte[] withCipher(CheckedFunction fn) throws SecurityException { + return b64Url(FIGURE_87) + } + } + } + } + + def enc = new GcmAesAeadAlgorithm(256) { + @Override + protected byte[] ensureInitializationVector(Request request) { + return b64Url(FIGURE_86) // ensure RFC required value + } + } + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 3, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + assertEquals enc.getId(), m.get('enc') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_88) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_72) + .encryptWith(key, alg, enc) + .compact() + + assertEquals FIGURE_92, result + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().decryptWith(jwk.toKey()).build().parseContentJwe(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals enc.getId(), parsed.header.getEncryptionAlgorithm() + assertEquals FIGURE_72, utf8(parsed.payload) + } + + @Test + void testSection5_3() { + + def key = Keys.forPassword(FIGURE_96.toCharArray()) + String cty = 'jwk-set+json' + int p2c = 8192 + + def wrapAlg = new AesWrapKeyAlgorithm(256) { + @Override + SecretKey generateKey(KeyRequest request) { + byte[] encoded = b64Url(FIGURE_97) // ensure RFC value + return new SecretKeySpec(encoded, "AES") + } + } + def alg = new Pbes2HsAkwAlgorithm(512, wrapAlg) { + @Override + protected byte[] generateInputSalt(KeyRequest request) { + return b64Url(FIGURE_99) // ensure RFC value + } + } + def enc = new HmacAesAeadAlgorithm(128) { + @Override + protected byte[] ensureInitializationVector(Request request) { + return b64Url(FIGURE_98) // ensure RFC value + } + } + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 5, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals FIGURE_99, m.get('p2s') + assertEquals p2c, m.get('p2c') + assertEquals cty, m.get('cty') + assertEquals enc.getId(), m.get('enc') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_101) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeader(Jwts.header().setContentType(cty).setPbes2Count(p2c)) + .setPayload(FIGURE_95) + .encryptWith(key, alg, enc) + .compact() + + assertEquals FIGURE_105, result + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().decryptWith(key).build().parseContentJwe(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals FIGURE_99, b64Url(parsed.header.getPbes2Salt()) + assertEquals p2c, parsed.header.getPbes2Count() + assertEquals cty, parsed.header.getContentType() + assertEquals enc.getId(), parsed.header.getEncryptionAlgorithm() + assertEquals FIGURE_95, utf8(parsed.payload) + } + + @Test + void testSection5_4() { + + def jwk = Jwks.parser().build().parse(FIGURE_108) as EcPrivateJwk + def encKey = jwk.toPublicJwk().toKey() + + def wrapAlg = new AesWrapKeyAlgorithm(128) { + @Override + SecretKey generateKey(KeyRequest request) { + byte[] encoded = b64Url(FIGURE_109) // ensure RFC value + return new SecretKeySpec(encoded, "AES") + } + } + def RFC_EPK = Jwks.parser().build().parse(FIGURE_111) as EcPrivateJwk + def alg = new EcdhKeyAlgorithm(wrapAlg) { + @Override + protected KeyPair generateKeyPair(Request request, ECParameterSpec spec) { + return new KeyPair(RFC_EPK.toPublicJwk().toKey(), RFC_EPK.toKey()) // ensure RFC value + } + } + def enc = new GcmAesAeadAlgorithm(128) { + @Override + protected byte[] ensureInitializationVector(Request request) { + return b64Url(FIGURE_110) + } + } + + // because Maps are not guaranteed to have the same order as defined in the RFC, we create an asserting + // serializer here to check the constructed data, and then, after guaranteeing the same data, return + // the order expected by the RFC + def serializer = new Serializer>() { + @Override + byte[] serialize(Map m) throws SerializationException { + assertEquals 4, m.size() + assertEquals alg.getId(), m.get('alg') + assertEquals jwk.getId(), m.get('kid') + assertEquals enc.getId(), m.get('enc') + assertEquals RFC_EPK.toPublicJwk(), m.get('epk') + //everything has been asserted per the RFC - return the exact order as shown in the RFC: + return utf8(FIGURE_113) + } + } + + String result = Jwts.builder() + .serializeToJsonWith(serializer) // assert input, return RFC ordered string + .setHeaderParam('kid', jwk.getId()) + .setPayload(FIGURE_72) + .encryptWith(encKey, alg, enc) + .compact() + + assertEquals FIGURE_117, result + + // Assert round trip works as expected: + def parsed = Jwts.parserBuilder().decryptWith(jwk.toKey()).build().parseContentJwe(result) + assertEquals alg.getId(), parsed.header.getAlgorithm() + assertEquals enc.getId(), parsed.header.getEncryptionAlgorithm() + assertEquals jwk.getId(), parsed.header.getKeyId() + assertEquals RFC_EPK.toPublicJwk(), parsed.header.getEphemeralPublicKey() + assertEquals FIGURE_72, utf8(parsed.payload) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7638Section3Dot1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7638Section3Dot1Test.groovy new file mode 100644 index 00000000..203b9d86 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7638Section3Dot1Test.groovy @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.RfcTests +import io.jsonwebtoken.security.Jwks +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.assertArrayEquals +import static org.junit.Assert.assertEquals + +class RFC7638Section3Dot1Test extends RfcTests { + + // defined in https://www.rfc-editor.org/rfc/rfc7638#section-3.1 + static final String KEY_JSON = stripws(''' + { + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt + VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6 + 4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD + W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9 + 1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH + aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AQAB", + "alg": "RS256", + "kid": "2011-04-29" + } +''') + + static final String KEY_THUMBPRINT_JSON = stripws(''' + {"e":"AQAB","kty":"RSA","n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2 + aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCi + FV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65Y + GjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n + 91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_x + BniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"} + ''') + + static final byte[] KEY_THUMBPRINT_JSON_UTF8_BYTES = + [123, 34, 101, 34, 58, 34, 65, 81, 65, 66, 34, 44, 34, 107, 116, 121, + 34, 58, 34, 82, 83, 65, 34, 44, 34, 110, 34, 58, 34, 48, 118, 120, + 55, 97, 103, 111, 101, 98, 71, 99, 81, 83, 117, 117, 80, 105, 76, 74, + 88, 90, 112, 116, 78, 57, 110, 110, 100, 114, 81, 109, 98, 88, 69, + 112, 115, 50, 97, 105, 65, 70, 98, 87, 104, 77, 55, 56, 76, 104, 87, + 120, 52, 99, 98, 98, 102, 65, 65, 116, 86, 84, 56, 54, 122, 119, 117, + 49, 82, 75, 55, 97, 80, 70, 70, 120, 117, 104, 68, 82, 49, 76, 54, + 116, 83, 111, 99, 95, 66, 74, 69, 67, 80, 101, 98, 87, 75, 82, 88, + 106, 66, 90, 67, 105, 70, 86, 52, 110, 51, 111, 107, 110, 106, 104, + 77, 115, 116, 110, 54, 52, 116, 90, 95, 50, 87, 45, 53, 74, 115, 71, + 89, 52, 72, 99, 53, 110, 57, 121, 66, 88, 65, 114, 119, 108, 57, 51, + 108, 113, 116, 55, 95, 82, 78, 53, 119, 54, 67, 102, 48, 104, 52, 81, + 121, 81, 53, 118, 45, 54, 53, 89, 71, 106, 81, 82, 48, 95, 70, 68, + 87, 50, 81, 118, 122, 113, 89, 51, 54, 56, 81, 81, 77, 105, 99, 65, + 116, 97, 83, 113, 122, 115, 56, 75, 74, 90, 103, 110, 89, 98, 57, 99, + 55, 100, 48, 122, 103, 100, 65, 90, 72, 122, 117, 54, 113, 77, 81, + 118, 82, 76, 53, 104, 97, 106, 114, 110, 49, 110, 57, 49, 67, 98, 79, + 112, 98, 73, 83, 68, 48, 56, 113, 78, 76, 121, 114, 100, 107, 116, + 45, 98, 70, 84, 87, 104, 65, 73, 52, 118, 77, 81, 70, 104, 54, 87, + 101, 90, 117, 48, 102, 77, 52, 108, 70, 100, 50, 78, 99, 82, 119, + 114, 51, 88, 80, 107, 115, 73, 78, 72, 97, 81, 45, 71, 95, 120, 66, + 110, 105, 73, 113, 98, 119, 48, 76, 115, 49, 106, 70, 52, 52, 45, 99, + 115, 70, 67, 117, 114, 45, 107, 69, 103, 85, 56, 97, 119, 97, 112, + 74, 122, 75, 110, 113, 68, 75, 103, 119, 34, 125] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7638#section-3.1 + static final byte[] KEY_S256_DIGEST = + [55, 54, 203, 177, 120, 124, 184, 48, 156, 119, 238, 140, 55, 5, 197, + 225, 111, 251, 158, 133, 151, 21, 144, 31, 30, 76, 89, 177, 17, 130, + 245, 123] as byte[] + + // defined in https://www.rfc-editor.org/rfc/rfc7638#section-3.1 + static final String KEY_S256_DIGEST_B64URL = 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs' as String + + /** + * Asserts we produce the expected output as shown in + * RFC 7638, Section 3.1. + */ + @Test + void testRFC7638Section3_1() { + //assert our test values are correct: + assertEquals(KEY_S256_DIGEST_B64URL, encode(KEY_S256_DIGEST)) + assertArrayEquals(KEY_THUMBPRINT_JSON_UTF8_BYTES, KEY_THUMBPRINT_JSON.getBytes(StandardCharsets.UTF_8)) + + def jwk = Jwks.parser().build().parse(KEY_JSON) + + def thumbprint = jwk.thumbprint() + + assertArrayEquals(KEY_S256_DIGEST, thumbprint.toByteArray()) + assertEquals(KEY_S256_DIGEST_B64URL, thumbprint.toString()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy new file mode 100644 index 00000000..f8465be4 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC8037AppendixATest.groovy @@ -0,0 +1,292 @@ +/* + * 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.RfcTests +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.OctetPrivateJwk +import io.jsonwebtoken.security.OctetPublicJwk +import org.junit.Test + +import java.nio.charset.StandardCharsets +import java.security.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +class RFC8037AppendixATest { + + // https://www.rfc-editor.org/rfc/rfc8037#appendix-A.1 : + static final String A1_ED25519_PRIVATE_JWK_STRING = RfcTests.stripws(''' + { + "kty":"OKP", + "crv":"Ed25519", + "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }''') + + // https://www.rfc-editor.org/rfc/rfc8037#appendix-A.2 + static final String A2_ED25519_PUBLIC_JWK_STRING = RfcTests.stripws(''' + { + "kty":"OKP","crv":"Ed25519", + "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }''') + + // https://www.rfc-editor.org/rfc/rfc8037#appendix-A.3 + static final A3_JWK_THUMBPRINT_B64URL = 'kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k' + static final A3_JWK_THUMMBPRINT_HEX = '90facafea9b1556698540f70c0117a22ea37bd5cf3ed3c47093c1707282b4b89' + + static final A4_JWS_PAYLOAD = 'Example of Ed25519 signing' + + static final String A4_JWS_COMPACT = RfcTests.stripws(''' + eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.hgyY0il_MGCj + P0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_Mu + M0KAg''') + + static OctetPrivateJwk a1Jwk() { + Jwks.parser().build().parse(A1_ED25519_PRIVATE_JWK_STRING) as OctetPrivateJwk + } + + static OctetPublicJwk a2Jwk() { + Jwks.parser().build().parse(A2_ED25519_PUBLIC_JWK_STRING) as OctetPublicJwk + } + + + @Test + void testSections_A1_A2_and_A3() { + def privJwk = a1Jwk() + assertTrue privJwk instanceof OctetPrivateJwk + PrivateKey privKey = privJwk.toKey() as PrivateKey + PublicKey pubKey = privJwk.toPublicJwk().toKey() as PublicKey + + def builtPrivJwk = Jwks.builder().forKey(privKey).setPublicKey(pubKey).build() + + //output should equal RFC input: + assertEquals privJwk, builtPrivJwk + + // Our built public JWK must reflect the RFC public JWK string value: + def a2PubJwk = a2Jwk() + assertTrue a2PubJwk instanceof OctetPublicJwk + PublicKey a2PubJwkKey = a2PubJwk.toKey() as PublicKey + + assertEquals a2PubJwk, privJwk.toPublicJwk() + assertEquals a2PubJwkKey, pubKey + + // Assert Section A.3 values: + def privThumbprint = privJwk.thumbprint() + def pubThumbprint = a2PubJwk.thumbprint() + assertEquals(privThumbprint, pubThumbprint) + + assertEquals A3_JWK_THUMBPRINT_B64URL, privThumbprint.toString() + assertEquals A3_JWK_THUMBPRINT_B64URL, pubThumbprint.toString() + + assertEquals A3_JWK_THUMMBPRINT_HEX, privThumbprint.toByteArray().encodeHex().toString() + assertEquals A3_JWK_THUMMBPRINT_HEX, pubThumbprint.toByteArray().encodeHex().toString() + } + + @Test + void test_Sections_A4_and_A5() { + def privJwk = a1Jwk() + String compact = Jwts.builder() + .setContent(A4_JWS_PAYLOAD.getBytes(StandardCharsets.UTF_8)) + .signWith(privJwk.toKey() as PrivateKey, Jwts.SIG.EdDSA) + .compact() + assertEquals A4_JWS_COMPACT, compact + + def pubJwk = a2Jwk() + def payloadBytes = Jwts.parserBuilder().verifyWith(pubJwk.toKey()).build().parse(compact).getPayload() as byte[] + def payload = new String(payloadBytes, StandardCharsets.UTF_8) + assertEquals A4_JWS_PAYLOAD, payload + } + + + /** + * https://www.rfc-editor.org/rfc/rfc8037#appendix-A indicates the public/private key pairs used for test + * vectors for sections A6 and A7 are defined in RFC 7748. + * Diffie-Hellman curve 25519 (X25519) test vectors are in + * RFC 7748, Section 6.1 specifically. + */ + @Test + void testSectionA6() { // defined in https://www.rfc-editor.org/rfc/rfc8037#appendix-A.6 + + // These two values are defined in https://www.rfc-editor.org/rfc/rfc7748#section-6.1: + def bobPubKeyHex = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f' + def bobPrivKeyHex = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb' + + //convert these two values to a JWK for convenient reference: + def bobPrivJwk = Jwks.builder().putAll([ + kty: "OKP", crv: "X25519", kid: "Bob", + x : bobPubKeyHex.decodeHex().encodeBase64Url() as String, + d : bobPrivKeyHex.decodeHex().encodeBase64Url() as String + ]).build() as OctetPrivateJwk + + // RFC-specified test vectors to be used during DH calculation: + def rfcEphemeralSecretHex = RfcTests.stripws(''' + 77 07 6d 0a 73 18 a5 7d 3c 16 c1 72 51 b2 66 45 + df 4c 2f 87 eb c0 99 2a b1 77 fb a5 1d b9 2c 2a''') + + def rfcEphemeralPubKeyHex = RfcTests.stripws(''' + 85 20 f0 09 89 30 a7 54 74 8b 7d dc b4 3e f7 5a + 0d bf 3a 0d 26 38 1a f4 eb a4 a9 8e aa 9b 4e 6a''') + + //Turn these two values into a Java KeyPair, and ensure it is used during key algorithm execution: + final OctetPrivateJwk ephemJwk = Jwks.builder().putAll([ + kty: "OKP", + crv: "X25519", + x : rfcEphemeralPubKeyHex.decodeHex().encodeBase64Url() as String, + d : rfcEphemeralSecretHex.decodeHex().encodeBase64Url() as String + ]).build() as OctetPrivateJwk + + // ensure this is used during key algorithm execution per the RFC test case: + def alg = new EcdhKeyAlgorithm(Jwts.KEY.A128KW) { + @Override + protected KeyPair generateKeyPair(SecureRandom random, EdwardsCurve curve, Provider provider) { + return ephemJwk.toKeyPair().toJavaKeyPair() + } + } + + // the RFC test vectors don't specify a JWE body/content, so we'll just add a random issuer claim and verify + // that on decryption: + final String issuer = RfcTests.srandom() + + // Create the test case JWE with the 'kid' header to ensure the output matches the RFC expected value: + String jwe = Jwts.builder() + .setHeaderParam('kid', bobPrivJwk.getId()) + .setIssuer(issuer) + .encryptWith(bobPrivJwk.toPublicJwk().toKey() as PublicKey, alg, Jwts.ENC.A128GCM) + .compact() + + // the constructed JWE should have the following protected header: + String rfcExpectedProtectedHeaderJson = RfcTests.stripws(''' + { + "alg": "ECDH-ES+A128KW", + "epk": { + "kty": "OKP", + "crv": "X25519", + "x": "hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo" + }, + "enc": "A128GCM", + "kid": "Bob" + }''') + + String jweHeaderJson = new String(jwe.substring(0, jwe.indexOf('.')).decodeBase64Url(), StandardCharsets.UTF_8) + + // since JSON key/value ordering in JSON strings is not guaranteed, we change them to Maps and do equality + // assertions that way: + def rfcExpectedHeaderMap = RfcTests.jsonToMap(rfcExpectedProtectedHeaderJson) + def jweHeaderMap = RfcTests.jsonToMap(jweHeaderJson) + assertEquals(rfcExpectedHeaderMap, jweHeaderMap) + assertEquals(rfcExpectedHeaderMap.get('epk'), jweHeaderMap.get('epk')) + + //ensure that bob can decrypt: + def jwt = Jwts.parserBuilder().decryptWith(bobPrivJwk.toKey() as PrivateKey).build().parseClaimsJwe(jwe) + + assertEquals(issuer, jwt.getPayload().getIssuer()) + } + + /** + * https://www.rfc-editor.org/rfc/rfc8037#appendix-A indicates the public/private key pairs used for test + * vectors for sections A6 and A7 are defined in RFC 7748. + * For Diffie-Hellman curve 448 (X448) test vectors are in + * RFC 7748, Section 6.2 specifically. + */ + @Test + void testSectionA7() { // defined in https://www.rfc-editor.org/rfc/rfc8037#appendix-A.7 + + // These two values are defined in https://www.rfc-editor.org/rfc/rfc7748#section-6.2 + // (Appendex A.7 oddly refers to this key holder as "Dave" when their own referenced RFC test vectors + // (RFC 7748, Section 6.2) calls this holder "Bob". We'll keep the 'bob' variable name references, but change + // the 'kid' value to "Dave" to match Section A.7 header values: + def bobPubKeyHex = '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609' + def bobPrivKeyHex = '1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d' + + //convert these two values to a JWK for convenient reference: + def bobPrivJwk = Jwks.builder().putAll([ + kty: "OKP", crv: "X448", kid: "Dave", // "Dave" instead of expected "Bob" + x : bobPubKeyHex.decodeHex().encodeBase64Url() as String, + d : bobPrivKeyHex.decodeHex().encodeBase64Url() as String + ]).build() as OctetPrivateJwk + + // RFC-specified test vectors to be used during DH calculation: + def rfcEphemeralSecretHex = RfcTests.stripws(''' + 9a 8f 49 25 d1 51 9f 57 75 cf 46 b0 4b 58 00 d4 + ee 9e e8 ba e8 bc 55 65 d4 98 c2 8d d9 c9 ba f5 + 74 a9 41 97 44 89 73 91 00 63 82 a6 f1 27 ab 1d + 9a c2 d8 c0 a5 98 72 6b''') + + def rfcEphemeralPubKeyHex = RfcTests.stripws(''' + 9b 08 f7 cc 31 b7 e3 e6 7d 22 d5 ae a1 21 07 4a + 27 3b d2 b8 3d e0 9c 63 fa a7 3d 2c 22 c5 d9 bb + c8 36 64 72 41 d9 53 d4 0c 5b 12 da 88 12 0d 53 + 17 7f 80 e5 32 c4 1f a0''') + + //Turn these two values into a Java KeyPair, and ensure it is used during key algorithm execution: + final OctetPrivateJwk ephemJwk = Jwks.builder().putAll([ + kty: "OKP", + crv: "X448", + x : rfcEphemeralPubKeyHex.decodeHex().encodeBase64Url() as String, + d : rfcEphemeralSecretHex.decodeHex().encodeBase64Url() as String + ]).build() as OctetPrivateJwk + + // ensure this is used during key algorithm execution per the RFC test case: + def alg = new EcdhKeyAlgorithm(Jwts.KEY.A256KW) { + @Override + protected KeyPair generateKeyPair(SecureRandom random, EdwardsCurve curve, Provider provider) { + return ephemJwk.toKeyPair().toJavaKeyPair() + } + } + + // the RFC test vectors don't specify a JWE body/content, so we'll just add a random issuer claim and verify + // that on decryption: + final String issuer = RfcTests.srandom() + + // Create the test case JWE with the 'kid' header to ensure the output matches the RFC expected value: + String jwe = Jwts.builder() + .setHeaderParam('kid', bobPrivJwk.getId()) //value will be "Dave" as noted above + .setIssuer(issuer) + .encryptWith(bobPrivJwk.toPublicJwk().toKey() as PublicKey, alg, Jwts.ENC.A256GCM) + .compact() + + // the constructed JWE should have the following protected header: + String rfcExpectedProtectedHeaderJson = RfcTests.stripws(''' + { + "alg": "ECDH-ES+A256KW", + "epk": { + "kty": "OKP", + "crv": "X448", + "x": "mwj3zDG34-Z9ItWuoSEHSic70rg94Jxj-qc9LCLF2bvINmRyQdlT1AxbEtqIEg1TF3-A5TLEH6A" + }, + "enc": "A256GCM", + "kid":"Dave" + }''') + + String jweHeaderJson = new String(jwe.substring(0, jwe.indexOf('.')).decodeBase64Url(), StandardCharsets.UTF_8) + + // since JSON key/value ordering in JSON strings is not guaranteed, we change them to Maps and do equality + // assertions that way: + def rfcExpectedHeaderMap = RfcTests.jsonToMap(rfcExpectedProtectedHeaderJson) + def jweHeaderMap = RfcTests.jsonToMap(jweHeaderJson) + assertEquals(rfcExpectedHeaderMap, jweHeaderMap) + assertEquals(rfcExpectedHeaderMap.get('epk'), jweHeaderMap.get('epk')) + + //ensure that Bob ("Dave") can decrypt: + def jwt = Jwts.parserBuilder().decryptWith(bobPrivJwk.toKey() as PrivateKey).build().parseClaimsJwe(jwe) + + //assert that we've decrypted and the value in the body/content is as expected: + assertEquals(issuer, jwt.getPayload().getIssuer()) + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy new file mode 100644 index 00000000..9820a99e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RSAOtherPrimeInfoConverterTest.groovy @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.security.MalformedKeyException +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail + +class RSAOtherPrimeInfoConverterTest { + + @Test + void testApplyFromNull() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom(null) + fail() + } catch (MalformedKeyException expected) { + String msg = 'RSA JWK \'oth\' (Other Prime Info) element cannot be null.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testApplyFromWithoutMap() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom(42) + fail() + } catch (MalformedKeyException expected) { + String msg = 'RSA JWK \'oth\' (Other Prime Info) must contain map elements of ' + + 'name/value pairs. Element type found: java.lang.Integer' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testApplyFromWithEmptyMap() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom([:]) + fail() + } catch (MalformedKeyException expected) { + String msg = 'RSA JWK \'oth\' (Other Prime Info) element map cannot be empty.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testApplyFromWithMalformedMap() { + try { + RSAOtherPrimeInfoConverter.INSTANCE.applyFrom(['r':2]) + fail() + } catch (MalformedKeyException expected) { + String msg = "Invalid JWK 'r' (Prime Factor) value: . Values must be either String or " + + "java.math.BigInteger instances. Value type found: java.lang.Integer." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy new file mode 100644 index 00000000..9cd5a914 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RandomsTest.groovy @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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.security + +import org.junit.Test + +import java.security.SecureRandom + +import static org.junit.Assert.assertTrue + +/** + * @since JJWT_RELEASE_VERSION + */ +class RandomsTest { + + @Test + void testPrivateCtor() { //for code coverage only + new Randoms() + } + + @Test + void testSecureRandom() { + def random = Randoms.secureRandom() + assertTrue random instanceof SecureRandom + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy new file mode 100644 index 00000000..ac824092 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaPrivateJwkFactoryTest.groovy @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.impl.lang.Converters +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.RsaPrivateJwk +import io.jsonwebtoken.security.UnsupportedKeyException +import org.junit.Test + +import java.security.interfaces.RSAMultiPrimePrivateCrtKey +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.KeySpec +import java.security.spec.RSAMultiPrimePrivateCrtKeySpec +import java.security.spec.RSAOtherPrimeInfo + +import static org.junit.Assert.* + +class RsaPrivateJwkFactoryTest { + + @Test + void testGetPublicExponentFailure() { + + def key = new TestRSAPrivateKey(null) { + @Override + BigInteger getModulus() { + return null + } + } + + try { + Jwks.builder().forKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String msg = 'Unable to derive RSAPublicKey from RSAPrivateKey implementation ' + + '[io.jsonwebtoken.impl.security.RsaPrivateJwkFactoryTest$1]. Supported keys implement the ' + + 'java.security.interfaces.RSAPrivateCrtKey or ' + + 'java.security.interfaces.RSAMultiPrimePrivateCrtKey interfaces. If the specified RSAPrivateKey ' + + 'cannot be one of these two, you must explicitly provide an RSAPublicKey in addition to the ' + + 'RSAPrivateKey, as the [JWA RFC, Section 6.3.2]' + + '(https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2) requires public values to be ' + + 'present in private RSA JWKs.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testFailedPublicKeyDerivation() { + def key = new RSAPrivateCrtKey() { + @Override + BigInteger getPublicExponent() { + return BigInteger.ZERO + } + + @Override + BigInteger getPrimeP() { + return null + } + + @Override + BigInteger getPrimeQ() { + return null + } + + @Override + BigInteger getPrimeExponentP() { + return null + } + + @Override + BigInteger getPrimeExponentQ() { + return null + } + + @Override + BigInteger getCrtCoefficient() { + return null + } + + @Override + BigInteger getPrivateExponent() { + return null + } + + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + + @Override + BigInteger getModulus() { + return BigInteger.ZERO + } + } as RSAPrivateKey + + try { + Jwks.builder().forKey(key).build() + fail() + } catch (UnsupportedKeyException expected) { + String prefix = 'Unable to derive RSAPublicKey from RSAPrivateKey {kty=RSA}. Cause: ' + assertTrue expected.getMessage().startsWith(prefix) + } + } + + @Test + void testMultiPrimePrivateKey() { + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey priv = pair.private as RSAPrivateCrtKey + + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.TEN, BigInteger.TEN, BigInteger.TEN) + def infos = [info1, info2] + + //build up test key: + RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, infos) + + RsaPrivateJwk jwk = Jwks.builder().forKey(key).build() + + List oth = jwk.get('oth') as List + assertTrue oth instanceof List + assertEquals 2, oth.size() + + Map one = oth.get(0) as Map + assertEquals one.r, RSAOtherPrimeInfoConverter.PRIME_FACTOR.applyTo(info1.prime) + assertEquals one.d, RSAOtherPrimeInfoConverter.FACTOR_CRT_EXPONENT.applyTo(info1.crtCoefficient) + assertEquals one.t, RSAOtherPrimeInfoConverter.FACTOR_CRT_COEFFICIENT.applyTo(info1.crtCoefficient) + + Map two = oth.get(1) as Map + assertEquals two.r, RSAOtherPrimeInfoConverter.PRIME_FACTOR.applyTo(info2.prime) + assertEquals two.d, RSAOtherPrimeInfoConverter.FACTOR_CRT_EXPONENT.applyTo(info2.crtCoefficient) + assertEquals two.t, RSAOtherPrimeInfoConverter.FACTOR_CRT_COEFFICIENT.applyTo(info2.crtCoefficient) + } + + @Test + void testMultiPrimePrivateKeyWithoutExtraInfo() { + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey priv = pair.private as RSAPrivateCrtKey + RSAPublicKey pub = pair.public as RSAPublicKey + + RsaPrivateJwk jwk = Jwks.builder().forKey(priv).setPublicKey(pub).build() + // an RSAMultiPrimePrivateCrtKey without OtherInfo elements is treated the same as a normal RSAPrivateCrtKey, + // so ensure they are equal: + RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, null) + RsaPrivateJwk jwk2 = Jwks.builder().forKey(key).setPublicKey(pub).build() + assertEquals jwk, jwk2 + assertNull jwk.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()) + assertNull jwk2.get(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO.getId()) + } + + @Test + void testNonCrtPrivateKey() { + //tests a standard RSAPrivateKey (not a RSAPrivateCrtKey or RSAMultiPrimePrivateCrtKey): + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey privCrtKey = pair.private as RSAPrivateCrtKey + RSAPublicKey pub = pair.public as RSAPublicKey + + def priv = new TestRSAPrivateKey(privCrtKey) + + RsaPrivateJwk jwk = Jwks.builder().forKey(priv).setPublicKey(pub).build() + assertEquals 4, jwk.size() // kty, public exponent, modulus, private exponent + assertEquals 'RSA', jwk.getType() + assertEquals Converters.BIGINT.applyTo(pub.getModulus()), jwk.get(DefaultRsaPublicJwk.MODULUS.getId()) + assertEquals Converters.BIGINT.applyTo(pub.getPublicExponent()), jwk.get(DefaultRsaPublicJwk.PUBLIC_EXPONENT.getId()) + assertEquals Converters.BIGINT.applyTo(priv.getPrivateExponent()), jwk.get(DefaultRsaPrivateJwk.PRIVATE_EXPONENT.getId()).get() + } + + @Test + void testCreateJwkFromMinimalValues() { // no optional private values + def pair = TestKeys.RS256.pair + RSAPublicKey pub = pair.public as RSAPublicKey + RSAPrivateKey priv = new TestRSAPrivateKey(pair.private as RSAPrivateKey) + def jwk = Jwks.builder().forKey(priv).setPublicKey(pub).build() + //minimal values: kty, modulus, public exponent, private exponent = 4 fields: + assertEquals 4, jwk.size() + def map = new LinkedHashMap(jwk) + assertEquals 4, map.size() + + def jwkFromValues = Jwks.builder().putAll(map).build() + + //ensure they're equal: + assertEquals jwk, jwkFromValues + } + + @Test + void testCreateJwkFromMultiPrimeValues() { + def pair = TestKeys.RS256.pair + RSAPrivateCrtKey priv = pair.private as RSAPrivateCrtKey + RSAPublicKey pub = pair.public as RSAPublicKey + + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.TEN, BigInteger.TEN, BigInteger.TEN) + def infos = [info1, info2] + RSAMultiPrimePrivateCrtKey key = new TestRSAMultiPrimePrivateCrtKey(priv, infos) + + final RsaPrivateJwk jwk = Jwks.builder().forKey(key).setPublicKey(pub).build() + + //we have to test the class directly and override, since the dummy MultiPrime values won't be accepted by the + //JVM: + def factory = new RsaPrivateJwkFactory() { + @Override + protected RSAPrivateKey generateFromSpec(JwkContext ctx, KeySpec keySpec) { + assertTrue keySpec instanceof RSAMultiPrimePrivateCrtKeySpec + RSAMultiPrimePrivateCrtKeySpec spec = (RSAMultiPrimePrivateCrtKeySpec) keySpec + assertEquals key.modulus, spec.modulus + assertEquals key.publicExponent, spec.publicExponent + assertEquals key.privateExponent, spec.privateExponent + assertEquals key.primeP, spec.primeP + assertEquals key.primeQ, spec.primeQ + assertEquals key.primeExponentP, spec.primeExponentP + assertEquals key.primeExponentQ, spec.primeExponentQ + assertEquals key.crtCoefficient, spec.crtCoefficient + + for (int i = 0; i < infos.size(); i++) { + RSAOtherPrimeInfo orig = infos.get(i) + RSAOtherPrimeInfo copy = spec.otherPrimeInfo[i] + assertEquals orig.prime, copy.prime + assertEquals orig.exponent, copy.exponent + assertEquals orig.crtCoefficient, copy.crtCoefficient + + } + return new TestRSAMultiPrimePrivateCrtKey(priv, infos) + } + } + + def returned = factory.createJwkFromValues(jwk.@context) + + assertEquals jwk, returned + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy new file mode 100644 index 00000000..d70dfb29 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.WeakKeyException +import org.junit.Test + +import java.security.KeyPairGenerator +import java.security.PublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.easymock.EasyMock.* +import static org.junit.Assert.* + +class RsaSignatureAlgorithmTest { + + static Collection algs() { + return Jwts.SIG.values().findAll({ + it.id.startsWith("RS") || it.id.startsWith("PS") + }) as Collection + } + + @Test + void testKeyPairBuilder() { + algs().each { + def pair = it.keyPairBuilder().build() + assertNotNull pair.public + assertTrue pair.public instanceof RSAPublicKey + assertEquals it.preferredKeyBitLength, pair.public.modulus.bitLength() + assertTrue pair.private instanceof RSAPrivateKey + assertEquals it.preferredKeyBitLength, pair.private.modulus.bitLength() + } + } + + @Test(expected = IllegalArgumentException) + void testWeakPreferredKeyLength() { + new RsaSignatureAlgorithm(256, 1024) //must be >= 2048 + } + + @Test + void testValidateKeyWithoutRsaKey() { + PublicKey key = createMock(PublicKey) + replay key + algs().each { + it.validateKey(key, false) + //no exception - can't check for RSAKey fields (e.g. PKCS11 or HSM key) + } + verify key + } + + @Test + void testValidateSigningKeyNotPrivate() { + RSAPublicKey key = createMock(RSAPublicKey) + def request = new DefaultSecureRequest(new byte[1], null, null, key) + try { + Jwts.SIG.RS256.digest(request) + fail() + } catch (InvalidKeyException e) { + assertTrue e.getMessage().startsWith("Asymmetric key signatures must be created with PrivateKeys. The specified key is of type: ") + } + } + + @Test + void testValidateSigningKeyWeakKey() { + def gen = KeyPairGenerator.getInstance("RSA") + gen.initialize(1024) //too week for any JWA RSA algorithm + def pair = gen.generateKeyPair() + + def request = new DefaultSecureRequest(new byte[1], null, null, pair.getPrivate()) + Jwts.SIG.values().findAll({ it.id.startsWith('RS') || it.id.startsWith('PS') }).each { + try { + it.digest(request) + fail() + } catch (WeakKeyException expected) { + } + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy new file mode 100644 index 00000000..f29617e5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.MalformedKeyException +import io.jsonwebtoken.security.SecretJwk +import org.junit.Test + +import static org.junit.Assert.* + +/** + * The {@link SecretJwkFactory} is tested in other classes (JwksTest, JwkParserTest, etc) - this class exists + * primarily to fill in coverage gaps where necessary. + * + * @since JJWT_RELEASE_VERSION + */ +class SecretJwkFactoryTest { + + @Test // if a jwk does not have an 'alg' or 'use' field, we default to an AES key + void testNoAlgNoSigJcaName() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() + SecretJwk result = Jwks.builder().putAll(jwk).build() as SecretJwk + assertEquals 'AES', result.toKey().getAlgorithm() + } + + @Test + void testJwkHS256AlgSetsKeyJcaNameCorrectly() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() + SecretJwk result = Jwks.builder().putAll(jwk).put('alg', 'HS256').build() as SecretJwk + assertEquals 'HmacSHA256', result.toKey().getAlgorithm() + } + + @Test + void testSignOpSetsKeyHmacSHA256() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() + SecretJwk result = Jwks.builder().putAll(jwk).setOperations(["sign"] as Set).build() as SecretJwk + assertNull result.getAlgorithm() + assertNull result.get('use') + assertEquals 'HmacSHA256', result.toKey().getAlgorithm() + } + + @Test + void testJwkHS384AlgSetsKeyJcaNameCorrectly() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS384).build() + SecretJwk result = Jwks.builder().putAll(jwk).put('alg', 'HS384').build() as SecretJwk + assertEquals 'HmacSHA384', result.toKey().getAlgorithm() + } + + @Test + void testSignOpSetsKeyHmacSHA384() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS384).build() + SecretJwk result = Jwks.builder().putAll(jwk).setOperations(["sign"] as Set).build() as SecretJwk + assertNull result.getAlgorithm() + assertNull result.get('use') + assertEquals 'HmacSHA384', result.toKey().getAlgorithm() + } + + @Test + void testJwkHS512AlgSetsKeyJcaNameCorrectly() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS512).build() + SecretJwk result = Jwks.builder().putAll(jwk).put('alg', 'HS512').build() as SecretJwk + assertEquals 'HmacSHA512', result.toKey().getAlgorithm() + } + + @Test + void testSignOpSetsKeyHmacSHA512() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS512).build() + SecretJwk result = Jwks.builder().putAll(jwk).setOperations(["sign"] as Set).build() as SecretJwk + assertNull result.getAlgorithm() + assertNull result.get('use') + assertEquals 'HmacSHA512', result.toKey().getAlgorithm() + } + + @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256 + void testNoAlgAndSigUseForHS256() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() + assertFalse jwk.containsKey('alg') + assertFalse jwk.containsKey('use') + SecretJwk result = Jwks.builder().putAll(jwk).put('use', 'sig').build() as SecretJwk + assertEquals 'HmacSHA256', result.toKey().getAlgorithm() // jcaName has been changed to a sig algorithm + } + + @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384 + void testNoAlgAndSigUseForHS384() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS384).build() + assertFalse jwk.containsKey('alg') + assertFalse jwk.containsKey('use') + SecretJwk result = Jwks.builder().putAll(jwk).put('use', 'sig').build() as SecretJwk + assertEquals 'HmacSHA384', result.toKey().getAlgorithm() + } + + @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512 + void testNoAlgAndSigUseForHS512() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS512).build() + assertFalse jwk.containsKey('alg') + assertFalse jwk.containsKey('use') + SecretJwk result = Jwks.builder().putAll(jwk).put('use', 'sig').build() as SecretJwk + assertEquals 'HmacSHA512', result.toKey().getAlgorithm() + } + + @Test // no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES + void testNoAlgAndNonSigUse() { + SecretJwk jwk = Jwks.builder().forKey(TestKeys.HS256).build() + assertFalse jwk.containsKey('alg') + assertFalse jwk.containsKey('use') + SecretJwk result = Jwks.builder().putAll(jwk).put('use', 'foo').build() as SecretJwk + assertEquals 'AES', result.toKey().getAlgorithm() + } + + /** + * Test the case where a jwk `alg` value is present, but the key material doesn't match that algs key length + * requirements. This would be a malformed key. + */ + @Test + void testSizeMismatchedSecretJwk() { + + //first get a valid HS256 JWK: + SecretJwk validJwk = Jwks.builder().forKey(TestKeys.HS256).build() + + //now associate it with an alg identifier that is more than the key is capable of: + try { + Jwks.builder().putAll(validJwk) + .put('alg', 'HS384') + .build() + fail() + } catch (MalformedKeyException expected) { + String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length does " + + "not equal the 'HS384' length requirement of 384 bits (48 bytes). This discrepancy could " + + "be the result of an algorithm substitution attack or simply an erroneously constructed " + + "JWK. In either case, it is likely to result in unexpected or undesired security consequences." + assertEquals msg, expected.getMessage() + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestCertificates.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestCertificates.groovy new file mode 100644 index 00000000..5b14008f --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestCertificates.groovy @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.lang.CheckedFunction +import io.jsonwebtoken.lang.Assert +import io.jsonwebtoken.lang.Classes +import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.SecureDigestAlgorithm +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter + +import java.nio.charset.StandardCharsets +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.Provider +import java.security.PublicKey +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.spec.X509EncodedKeySpec + +/** + * For test cases that need to read certificate and/or PEM files. Encapsulates BouncyCastle API to + * this class so it doesn't need to propagate across other test classes. + * + * MAINTAINERS NOTE: + * + * If this logic is ever needed in the impl or api modules, do not keep the + * name of this class - it was quickly thrown together and it isn't appropriately named for exposure in a public + * module. Thought/design is necessary to see if/how cert/pem reading should be exposed in an easy-to-use and + * maintain API (e.g. probably a builder). + * + * The only purpose of this class and its methods are to: + * 1) be used in Test classes only, and + * 2) encapsulate the BouncyCastle API so it is not exposed to other Test classes. + */ +class TestCertificates { + + private static InputStream getResourceStream(String filename) { + String packageName = TestCertificates.class.getPackage().getName() + String resourcePath = Strings.replace(packageName, ".", "/") + "/" + filename + return Classes.getResourceAsStream(resourcePath) + } + + private static PEMParser getParser(String filename) { + InputStream is = Classes.getResourceAsStream('io/jsonwebtoken/impl/security/' + filename) + return new PEMParser(new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1))) + } + + private static String getKeyFilePrefix(SecureDigestAlgorithm alg) { + if (alg instanceof EdSignatureAlgorithm) { + return alg.preferredCurve.getId() + } + return alg.getId() + } + + static X509Certificate readTestCertificate(SecureDigestAlgorithm alg) { + InputStream is = getResourceStream(getKeyFilePrefix(alg) + '.crt.pem') + try { + JcaTemplate template = new JcaTemplate("X.509", alg.getProvider()) + template.withCertificateFactory(new CheckedFunction() { + @Override + X509Certificate apply(CertificateFactory factory) throws Exception { + return (X509Certificate) factory.generateCertificate(is) + } + }) + } finally { + is.close() + } + } + + static PublicKey readTestPublicKey(EdwardsCurve crv) { + PEMParser parser = getParser(crv.getId() + '.pub.pem') + try { + SubjectPublicKeyInfo info = parser.readObject() as SubjectPublicKeyInfo + def template = new JcaTemplate(crv.getJcaName(), crv.getProvider()) + return template.withKeyFactory(new CheckedFunction() { + @Override + PublicKey apply(KeyFactory keyFactory) throws Exception { + return keyFactory.generatePublic(new X509EncodedKeySpec(info.getEncoded())) + } + }) + } finally { + parser.close() + } + } + + static PrivateKey readTestPrivateKey(SecureDigestAlgorithm alg) { + return readTestPrivateKey(getKeyFilePrefix(alg), alg.getProvider()) + } + + static PrivateKey readTestPrivateKey(String filenamePrefix, Provider provider) { + PEMParser parser = getParser(filenamePrefix + '.key.pem') + try { + PrivateKeyInfo info + Object object = parser.readObject() + if (object instanceof PEMKeyPair) { + info = ((PEMKeyPair) object).getPrivateKeyInfo() + } else { + info = (PrivateKeyInfo) object + } + + def converter = new JcaPEMKeyConverter() + if (provider != null) { + converter.setProvider(provider) + } else if (filenamePrefix.startsWith("X") && System.getProperty("java.version").startsWith("11")) { + EdwardsCurve curve = EdwardsCurve.findById(filenamePrefix) + Assert.notNull(curve, "Curve cannot be null.") + int expectedByteLen = ((curve.keyBitLength + 7) / 8) as int + // Address the [JDK 11 SunCE provider bug](https://bugs.openjdk.org/browse/JDK-8213363) for X25519 + // and X448 encoded keys: Even though the file is encoded properly (it was created by OpenSSL), JDK 11's + // SunCE provider incorrectly expects an ASN.1 OCTET STRING (without the DER tag/length prefix) + // when it should actually be a BER-encoded OCTET STRING (with the tag/length prefix). + // So we get the raw bytes and use our key generator: + byte[] keyOctets = info.getPrivateKey().getOctets() + int lenDifference = Bytes.length(keyOctets) - expectedByteLen + if (lenDifference > 0) { + byte[] derPrefixRemoved = new byte[expectedByteLen] + System.arraycopy(keyOctets, lenDifference, derPrefixRemoved, 0, expectedByteLen) + keyOctets = derPrefixRemoved + } + return curve.toPrivateKey(keyOctets, null) + } + return converter.getPrivateKey(info) + } finally { + parser.close() + } + } + + static TestKeys.Bundle readBundle(EdwardsCurve curve) { + PublicKey pub = readTestPublicKey(curve) + PrivateKey priv = readTestPrivateKey(curve.getId(), curve.getProvider()) + return new TestKeys.Bundle(pub, priv) + } + + static TestKeys.Bundle readAsymmetricBundle(SecureDigestAlgorithm alg) { + X509Certificate cert = readTestCertificate(alg) + PrivateKey priv = readTestPrivateKey(alg) + return new TestKeys.Bundle(cert, priv) + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy similarity index 70% rename from impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy index e0e46067..90eb9ae1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECField.groovy @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 jsonwebtoken.io + * Copyright (C) 2022 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jsonwebtoken.impl.crypto; +package io.jsonwebtoken.impl.security -import io.jsonwebtoken.SignatureAlgorithm; +import java.security.spec.ECField -import java.security.Key; +class TestECField implements ECField { -public interface SignerFactory { + int fieldSize - Signer createSigner(SignatureAlgorithm alg, Key key); + @Override + int getFieldSize() { + return fieldSize + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy new file mode 100644 index 00000000..54d91249 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECKey.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.interfaces.ECKey +import java.security.spec.ECParameterSpec + +class TestECKey extends TestKey implements ECKey { + + ECParameterSpec params + + @Override + ECParameterSpec getParams() { + return params + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy new file mode 100644 index 00000000..c8e8d3fa --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPrivateKey.groovy @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.interfaces.ECPrivateKey + +class TestECPrivateKey extends TestECKey implements ECPrivateKey { + + BigInteger s + + @Override + BigInteger getS() { + return s + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy new file mode 100644 index 00000000..251d0e53 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestECPublicKey.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.interfaces.ECPublicKey +import java.security.spec.ECPoint + +class TestECPublicKey extends TestECKey implements ECPublicKey { + + ECPoint w + + @Override + ECPoint getW() { + return w + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy new file mode 100644 index 00000000..c5000e1e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKey.groovy @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.Key + +class TestKey implements Key { + + String algorithm + String format + byte[] encoded + + @Override + String getAlgorithm() { + return algorithm + } + + @Override + String getFormat() { + return format + } + + @Override + byte[] getEncoded() { + return encoded + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy new file mode 100644 index 00000000..1fa679cb --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestKeys.groovy @@ -0,0 +1,134 @@ +/* + * 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.security + +import io.jsonwebtoken.Identifiable +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.KeyBuilderSupplier +import io.jsonwebtoken.security.SecretKeyBuilder +import io.jsonwebtoken.security.SignatureAlgorithm + +import javax.crypto.SecretKey +import java.security.KeyPair +import java.security.PrivateKey +import java.security.PublicKey +import java.security.cert.X509Certificate + +/** + * Test helper with cached keys to save time across tests (so we don't have to constantly dynamically generate keys) + */ +class TestKeys { + + // ======================================================= + // Secret Keys + // ======================================================= + static SecretKey HS256 = Jwts.SIG.HS256.keyBuilder().build() + static SecretKey HS384 = Jwts.SIG.HS384.keyBuilder().build() + static SecretKey HS512 = Jwts.SIG.HS512.keyBuilder().build() + static Collection HS = Collections.setOf(HS256, HS384, HS512) + + static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW + static Collection AGCM + static { + A128GCM = A128KW = A128GCMKW = Jwts.ENC.A128GCM.keyBuilder().build() + A192GCM = A192KW = A192GCMKW = Jwts.ENC.A192GCM.keyBuilder().build() + A256GCM = A256KW = A256GCMKW = Jwts.ENC.A256GCM.keyBuilder().build() + AGCM = Collections.setOf(A128GCM, A192GCM, A256GCM) + } + + static SecretKey A128CBC_HS256 = Jwts.ENC.A128CBC_HS256.keyBuilder().build() + static SecretKey A192CBC_HS384 = Jwts.ENC.A192CBC_HS384.keyBuilder().build() + static SecretKey A256CBC_HS512 = Jwts.ENC.A256CBC_HS512.keyBuilder().build() + static Collection ACBC = Collections.setOf(A128CBC_HS256, A192CBC_HS384, A256CBC_HS512) + + static Collection SECRET = new LinkedHashSet<>() + static { + SECRET.addAll(HS) + SECRET.addAll(AGCM) + SECRET.addAll(ACBC) + } + + // ======================================================= + // Elliptic Curve Keys & Certificates + // ======================================================= + static Bundle ES256 = TestCertificates.readAsymmetricBundle(Jwts.SIG.ES256) + static Bundle ES384 = TestCertificates.readAsymmetricBundle(Jwts.SIG.ES384) + static Bundle ES512 = TestCertificates.readAsymmetricBundle(Jwts.SIG.ES512) + static Set EC = Collections.setOf(ES256, ES384, ES512) + + static Bundle EdDSA = TestCertificates.readAsymmetricBundle(Jwts.SIG.EdDSA) + static Bundle Ed25519 = TestCertificates.readAsymmetricBundle(Jwts.SIG.Ed25519) + static Bundle Ed448 = TestCertificates.readAsymmetricBundle(Jwts.SIG.Ed448) + static Bundle X25519 = TestCertificates.readBundle(EdwardsCurve.X25519) + static Bundle X448 = TestCertificates.readBundle(EdwardsCurve.X448) + static Set EdEC = Collections.setOf(EdDSA, Ed25519, Ed448, X25519, X448) + + // ======================================================= + // RSA Keys & Certificates + // ======================================================= + static Bundle RS256 = TestCertificates.readAsymmetricBundle(Jwts.SIG.RS256) + static Bundle RS384 = TestCertificates.readAsymmetricBundle(Jwts.SIG.RS384) + static Bundle RS512 = TestCertificates.readAsymmetricBundle(Jwts.SIG.RS512) + static Set RSA = Collections.setOf(RS256, RS384, RS512) + + static Set ASYM = new LinkedHashSet<>() + static { + ASYM.addAll(EC) + ASYM.addAll(EdEC) + ASYM.addAll(RSA) + } + + static & Identifiable> SecretKey forAlgorithm(T alg) { + String id = alg.getId() + if (id.contains('-')) { + id = id.replace('-', '_') + } + return TestKeys.metaClass.getAttribute(TestKeys, id) as SecretKey + } + + static Bundle forAlgorithm(SignatureAlgorithm alg) { + String id = alg.getId() + if (id.startsWith('PS')) { + id = 'R' + id.substring(1) //keys for PS* algs are the same as RS algs + } + if (alg instanceof EdSignatureAlgorithm) { + id = alg.preferredCurve.getId() + } + return TestKeys.metaClass.getAttribute(TestKeys, id) as Bundle + } + + static Bundle forCurve(EdwardsCurve curve) { + return TestKeys.metaClass.getAttribute(TestKeys, curve.getId()) as Bundle + } + + static class Bundle { + X509Certificate cert + List chain + KeyPair pair + + Bundle(X509Certificate cert, PrivateKey privateKey) { + this.cert = cert + this.chain = Collections.of(cert) + this.pair = new KeyPair(cert.getPublicKey(), privateKey) + } + Bundle(PublicKey pub, PrivateKey priv) { + this.cert = null + this.chain = Collections.emptyList() + this.pair = new KeyPair(pub, priv) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestPrivateKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestPrivateKey.groovy new file mode 100644 index 00000000..3ad8d708 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestPrivateKey.groovy @@ -0,0 +1,35 @@ +/* + * 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.security + +import javax.security.auth.DestroyFailedException +import javax.security.auth.Destroyable +import java.security.PrivateKey + +class TestPrivateKey extends TestKey implements PrivateKey, Destroyable { + + boolean destroyed + + @Override + void destroy() throws DestroyFailedException { + destroyed = true + } + + @Override + boolean isDestroyed() { + return destroyed + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestPublicKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestPublicKey.groovy new file mode 100644 index 00000000..fe2242b2 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestPublicKey.groovy @@ -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.security + +import java.security.PublicKey + +class TestPublicKey extends TestKey implements PublicKey { +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy new file mode 100644 index 00000000..c0c4b8d9 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAKey.groovy @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.interfaces.RSAKey + +class TestRSAKey extends TestKey implements RSAKey { + + final def src + + TestRSAKey(def key) { + this.src = key + } + + @Override + String getAlgorithm() { + return src.algorithm + } + + @Override + String getFormat() { + return src.format + } + + @Override + byte[] getEncoded() { + return src.encoded + } + + @Override + BigInteger getModulus() { + return src.getModulus() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy new file mode 100644 index 00000000..3467d5c5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAMultiPrimePrivateCrtKey.groovy @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.interfaces.RSAMultiPrimePrivateCrtKey +import java.security.interfaces.RSAPrivateCrtKey +import java.security.spec.RSAOtherPrimeInfo + +class TestRSAMultiPrimePrivateCrtKey extends TestRSAPrivateKey implements RSAMultiPrimePrivateCrtKey { + + private final List infos + + TestRSAMultiPrimePrivateCrtKey(RSAPrivateCrtKey src, List infos) { + super(src) + this.infos = infos + } + + @Override + BigInteger getPublicExponent() { + return src.publicExponent + } + + @Override + BigInteger getPrimeP() { + return src.primeP + } + + @Override + BigInteger getPrimeQ() { + return src.primeQ + } + + @Override + BigInteger getPrimeExponentP() { + return src.primeExponentP + } + + @Override + BigInteger getPrimeExponentQ() { + return src.primeExponentQ + } + + @Override + BigInteger getCrtCoefficient() { + return src.crtCoefficient + } + + @Override + RSAOtherPrimeInfo[] getOtherPrimeInfo() { + return infos as RSAOtherPrimeInfo[] + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy new file mode 100644 index 00000000..c5ae31c5 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestRSAPrivateKey.groovy @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.interfaces.RSAPrivateKey + +class TestRSAPrivateKey extends TestRSAKey implements RSAPrivateKey { + + TestRSAPrivateKey(RSAPrivateKey key) { + super(key) + } + + @Override + BigInteger getPrivateExponent() { + return src.privateExponent + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy new file mode 100644 index 00000000..51842603 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestSecretKey.groovy @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 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.security + +import javax.crypto.SecretKey + +class TestSecretKey extends TestKey implements SecretKey { +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy new file mode 100644 index 00000000..1ceadb8c --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestX509Certificate.groovy @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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.security + +import java.security.* +import java.security.cert.* + +class TestX509Certificate extends X509Certificate { + + private boolean[] keyUsage = new boolean[9] + + @Override + void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + int getVersion() { + return 0 + } + + @Override + BigInteger getSerialNumber() { + return null + } + + @Override + Principal getIssuerDN() { + return null + } + + @Override + Principal getSubjectDN() { + return null + } + + @Override + Date getNotBefore() { + return null + } + + @Override + Date getNotAfter() { + return null + } + + @Override + byte[] getTBSCertificate() throws CertificateEncodingException { + return new byte[0] + } + + @Override + byte[] getSignature() { + return new byte[0] + } + + @Override + String getSigAlgName() { + return null + } + + @Override + String getSigAlgOID() { + return null + } + + @Override + byte[] getSigAlgParams() { + return new byte[0] + } + + @Override + boolean[] getIssuerUniqueID() { + return new boolean[0] + } + + @Override + boolean[] getSubjectUniqueID() { + return new boolean[0] + } + + @Override + boolean[] getKeyUsage() { + return this.keyUsage + } + + @Override + int getBasicConstraints() { + return 0 + } + + @Override + byte[] getEncoded() throws CertificateEncodingException { + return new byte[0] + } + + @Override + void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + String toString() { + return null + } + + @Override + PublicKey getPublicKey() { + return null + } + + @Override + boolean hasUnsupportedCriticalExtension() { + return false + } + + @Override + Set getCriticalExtensionOIDs() { + return null + } + + @Override + Set getNonCriticalExtensionOIDs() { + return null + } + + @Override + byte[] getExtensionValue(String oid) { + return new byte[0] + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy new file mode 100644 index 00000000..0d6d1154 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2018 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.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.security.DefaultAeadRequest +import io.jsonwebtoken.impl.security.DefaultAeadResult +import io.jsonwebtoken.impl.security.GcmAesAeadAlgorithm +import org.junit.Test + +import static org.junit.Assert.* + +/** + * Tests the {@link Jwts#ENC} implementation. + * + * @since JJWT_RELEASE_VERSION + */ +class EncryptionAlgorithmsTest { + + private static final String PLAINTEXT = + '''Bacon ipsum dolor amet venison beef pork chop, doner jowl pastrami ground round alcatra. + Beef leberkas filet mignon ball tip pork spare ribs kevin short loin ribeye ground round + biltong jerky short ribs corned beef. Strip steak turducken meatball porchetta beef ribs + shoulder pork belly doner salami corned beef kielbasa cow filet mignon drumstick. Bacon + tenderloin pancetta flank frankfurter ham kevin leberkas meatball turducken beef ribs. + Cupim short loin short ribs shankle tenderloin. Ham ribeye hamburger flank tenderloin + cupim t-bone, shank tri-tip venison salami sausage pancetta. Pork belly chuck salami + alcatra sirloin. + + 以ケ ホゥ婧詃 橎ちゅぬ蛣埣 禧ざしゃ蟨廩 椥䤥グ曣わ 基覧 滯っ䶧きょメ Ủ䧞以ケ妣 择禤槜谣お 姨のドゥ, + らボみょば䪩 苯礊觊ツュ婃 䩦ディふげセ げセりょ 禤槜 Ủ䧞以ケ妣 せがみゅちょ䰯 择禤槜谣お 難ゞ滧 蝥ちゃ, + 滯っ䶧きょメ らボみょば䪩 礯みゃ楦と饥 椥䤥グ ウァ槚 訤をりゃしゑ びゃ驨も氩簥 栨キョ奎婨榞 ヌに楃 以ケ, + 姚奊べ 椥䤥グ曣わ 栨キョ奎婨榞 ちょ䰯 Ủ䧞以ケ妣 誧姨のドゥろ よ苯礊 く涥, りゅぽ槞 馣ぢゃ尦䦎ぎ + 大た䏩䰥ぐ 郎きや楺橯 䧎キェ, 難ゞ滧 栧择 谯䧟簨訧ぎょ 椥䤥グ曣わ''' + + private static final byte[] PLAINTEXT_BYTES = PLAINTEXT.getBytes("UTF-8") + + private static final String AAD = 'You can get with this, or you can get with that' + private static final byte[] AAD_BYTES = AAD.getBytes("UTF-8") + + static boolean contains(AeadAlgorithm alg) { + return Jwts.ENC.values().contains(alg) + } + + @Test + void testValues() { + assertEquals 6, Jwts.ENC.values().size() + assertTrue(contains(Jwts.ENC.A128CBC_HS256) && + contains(Jwts.ENC.A192CBC_HS384) && + contains(Jwts.ENC.A256CBC_HS512) && + contains(Jwts.ENC.A128GCM) && + contains(Jwts.ENC.A192GCM) && + contains(Jwts.ENC.A256GCM) + ) + } + + @Test + void testForId() { + for (AeadAlgorithm alg : Jwts.ENC.values()) { + assertSame alg, Jwts.ENC.get(alg.getId()) + } + } + + @Test + void testForIdCaseInsensitive() { + for (AeadAlgorithm alg : Jwts.ENC.values()) { + assertSame alg, Jwts.ENC.get(alg.getId().toLowerCase()) + } + } + + @Test(expected = IllegalArgumentException) + void testForIdWithInvalidId() { + //unlike the 'find' paradigm, 'for' requires the value to exist + Jwts.ENC.get('invalid') + } + + @Test + void testFindById() { + for (AeadAlgorithm alg : Jwts.ENC.values()) { + assertSame alg, Jwts.ENC.find(alg.getId()) + } + } + + @Test + void testFindByIdCaseInsensitive() { + for (AeadAlgorithm alg : Jwts.ENC.values()) { + assertSame alg, Jwts.ENC.find(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull Jwts.ENC.find('invalid') + } + + @Test + void testWithoutAad() { + + for (AeadAlgorithm alg : Jwts.ENC.values()) { + + def key = alg.keyBuilder().build() + + def request = new DefaultAeadRequest(PLAINTEXT_BYTES, key, null) + + def result = alg.encrypt(request) + + byte[] tag = result.getDigest() //there is always a tag, even if there is no AAD + assertNotNull tag + + byte[] ciphertext = result.getPayload() + + boolean gcm = alg instanceof GcmAesAeadAlgorithm + + if (gcm) { //AES GCM always results in ciphertext the same length as the plaintext: + assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) + } + + def dreq = new DefaultAeadResult(null, null, ciphertext, key, null, tag, result.getInitializationVector()) + + byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload() + + assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) + } + } + + @Test + void testWithAad() { + + for (AeadAlgorithm alg : Jwts.ENC.values()) { + + def key = alg.keyBuilder().build() + + def req = new DefaultAeadRequest(PLAINTEXT_BYTES, null, null, key, AAD_BYTES) + + def result = alg.encrypt(req) + + byte[] ciphertext = result.getPayload() + + boolean gcm = alg instanceof GcmAesAeadAlgorithm + + if (gcm) { + assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) + } + + def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector()) + byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload() + assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy new file mode 100644 index 00000000..9ff97e8e --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeyAlgorithmsTest.groovy @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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.security + +import io.jsonwebtoken.Jwts +import org.junit.Test + +import java.security.Key + +import static org.junit.Assert.* + +/** + * Tests {@link Jwts#KEY} values. + * + * @since JJWT_RELEASE_VERSION + */ +class KeyAlgorithmsTest { + + static boolean contains(KeyAlgorithm alg) { + return Jwts.KEY.values().contains(alg) + } + + @Test + void testValues() { + assertEquals 17, Jwts.KEY.values().size() + assertTrue(contains(Jwts.KEY.DIRECT) && + contains(Jwts.KEY.A128KW) && + contains(Jwts.KEY.A192KW) && + contains(Jwts.KEY.A256KW) && + contains(Jwts.KEY.A128GCMKW) && + contains(Jwts.KEY.A192GCMKW) && + contains(Jwts.KEY.A256GCMKW) && + contains(Jwts.KEY.PBES2_HS256_A128KW) && + contains(Jwts.KEY.PBES2_HS384_A192KW) && + contains(Jwts.KEY.PBES2_HS512_A256KW) && + contains(Jwts.KEY.RSA1_5) && + contains(Jwts.KEY.RSA_OAEP) && + contains(Jwts.KEY.RSA_OAEP_256) && + contains(Jwts.KEY.ECDH_ES) && + contains(Jwts.KEY.ECDH_ES_A128KW) && + contains(Jwts.KEY.ECDH_ES_A192KW) && + contains(Jwts.KEY.ECDH_ES_A256KW) + ) + } + + @Test + void testForId() { + for (KeyAlgorithm alg : Jwts.KEY.values()) { + assertSame alg, Jwts.KEY.get(alg.getId()) + } + } + + @Test + void testForIdCaseInsensitive() { + for (KeyAlgorithm alg : Jwts.KEY.values()) { + assertSame alg, Jwts.KEY.get(alg.getId().toLowerCase()) + } + } + + @Test(expected = IllegalArgumentException) + void testForIdWithInvalidId() { + //unlike the 'find' paradigm, 'get' requires the value to exist + Jwts.KEY.get('invalid') + } + + @Test + void testFindById() { + for (KeyAlgorithm alg : Jwts.KEY.values()) { + assertSame alg, Jwts.KEY.find(alg.getId()) + } + } + + @Test + void testFindByIdCaseInsensitive() { + for (KeyAlgorithm alg : Jwts.KEY.values()) { + assertSame alg, Jwts.KEY.find(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull Jwts.KEY.find('invalid') + } + + /* + @Test + @Ignore // temporarily until we decide if this API will remain + void testEstimateIterations() { + // keep it super short so we don't hammer the test server or slow down the build too much: + long desiredMillis = 50 + int result = Jwts.KEY.estimateIterations(Jwts.KEY.PBES2_HS256_A128KW, desiredMillis) + assertTrue result > Pbes2HsAkwAlgorithm.MIN_RECOMMENDED_ITERATIONS + } + */ +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy new file mode 100644 index 00000000..76c5e2dc --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2014 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. + */ +//file:noinspection GrDeprecatedAPIUsage +package io.jsonwebtoken.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.DefaultJwtBuilder +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.security.* +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.security.KeyPair +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +import static org.junit.Assert.* + +@SuppressWarnings('GroovyAccessibility') +class KeysTest { + + private static final Random RANDOM = new SecureRandom() + + static byte[] bytes(int sizeInBits) { + byte[] bytes = new byte[sizeInBits / Byte.SIZE] + RANDOM.nextBytes(bytes) + return bytes + } + + @Test + void testPrivateCtor() { //for code coverage purposes only + //noinspection GroovyResultOfObjectAllocationIgnored + new Keys() + new KeysBridge() + } + + @Test + void testHmacShaKeyForWithNullArgument() { + try { + Keys.hmacShaKeyFor(null) + } catch (InvalidKeyException expected) { + assertEquals 'SecretKey byte array cannot be null.', expected.message + } + } + + @Test + void testHmacShaKeyForWithWeakKey() { + int numBytes = 31 + int numBits = numBytes * 8 + try { + Keys.hmacShaKeyFor(new byte[numBytes]) + } catch (WeakKeyException expected) { + assertEquals "The specified key byte array is " + numBits + " bits which " + + "is not secure enough for any JWT HMAC-SHA algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " + + "size >= 256 bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the StandardSecureDigestAlgorithms.HS256.keyBuilder() method (or " + + "HS384.keyBuilder() or HS512.keyBuilder()) to create a key guaranteed to be secure enough " + + "for your preferred HMAC-SHA algorithm. See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message + } + } + + @Test + void testHmacShaWithValidSizes() { + for (int i : [256, 384, 512]) { + byte[] bytes = bytes(i) + def key = Keys.hmacShaKeyFor(bytes) + assertTrue key instanceof SecretKeySpec + assertEquals "HmacSHA$i" as String, key.getAlgorithm() + assertTrue Arrays.equals(bytes, key.getEncoded()) + } + } + + @Test + void testHmacShaLargerThan512() { + def key = Keys.hmacShaKeyFor(bytes(520)) + assertTrue key instanceof SecretKeySpec + assertEquals 'HmacSHA512', key.getAlgorithm() + assertTrue key.getEncoded().length * Byte.SIZE >= 512 + } + + @Test + @Deprecated + void testDeprecatedSecretKeyFor() { + + for (io.jsonwebtoken.SignatureAlgorithm alg : io.jsonwebtoken.SignatureAlgorithm.values()) { + + String name = alg.name() + + if (alg.isHmac()) { + SecretKey key = Keys.secretKeyFor(alg) + assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count + assertEquals alg.jcaName, key.algorithm + alg.assertValidSigningKey(key) + alg.assertValidVerificationKey(key) + assertEquals alg, io.jsonwebtoken.SignatureAlgorithm.forSigningKey(key) + // https://github.com/jwtk/jjwt/issues/381 + } else { + try { + Keys.secretKeyFor(alg) + fail() + } catch (IllegalArgumentException expected) { + assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message + } + + } + } + } + + @Test + void testSecretKeyFor() { + for (SecureDigestAlgorithm alg : Jwts.SIG.values()) { + if (alg instanceof MacAlgorithm) { + SecretKey key = alg.keyBuilder().build() + assertEquals alg.getKeyBitLength(), Bytes.bitLength(key.getEncoded()) + assertEquals alg.jcaName, key.algorithm + assertEquals alg, DefaultJwtBuilder.forSigningKey(key) // https://github.com/jwtk/jjwt/issues/381 + } + } + } + + @Test + @Deprecated + void testDeprecatedKeyPairFor() { + + for (io.jsonwebtoken.SignatureAlgorithm alg : io.jsonwebtoken.SignatureAlgorithm.values()) { + + String name = alg.name() + + if (alg.isRsa()) { + + KeyPair pair = Keys.keyPairFor(alg) + assertNotNull pair + + PublicKey pub = pair.getPublic() + assert pub instanceof RSAPublicKey + assertEquals alg.familyName, pub.algorithm + assertEquals alg.digestLength * 8, pub.modulus.bitLength() + + PrivateKey priv = pair.getPrivate() + assert priv instanceof RSAPrivateKey + assertEquals alg.familyName, priv.algorithm + assertEquals alg.digestLength * 8, priv.modulus.bitLength() + + } else if (alg.isEllipticCurve()) { + + KeyPair pair = Keys.keyPairFor(alg) + assertNotNull pair + + int len = alg.minKeyLength + String asn1oid = "secp${len}r1" + String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' + //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names + String jdkParamName = "$asn1oid [NIST P-${len}${suffix}]" as String + + PublicKey pub = pair.getPublic() + assert pub instanceof ECPublicKey + assertEquals "EC", pub.algorithm + if (pub.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, pub.params.name + } else { // JDK >= 15 + assertEquals asn1oid, pub.params.nameAndAliases[0] + } + assertEquals alg.minKeyLength, pub.params.order.bitLength() + + PrivateKey priv = pair.getPrivate() + assert priv instanceof ECPrivateKey + assertEquals "EC", priv.algorithm + if (priv.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, priv.params.name + } else { // JDK >= 15 + assertEquals asn1oid, priv.params.nameAndAliases[0] + } + assertEquals alg.minKeyLength, priv.params.order.bitLength() + + } else { + try { + Keys.keyPairFor(alg) + fail() + } catch (IllegalArgumentException expected) { + assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message + } + } + } + } + + @Test + void testKeyPairFor() { + + for (SecureDigestAlgorithm alg : Jwts.SIG.values()) { + + if (alg instanceof RsaSignatureAlgorithm) { + + def pair = alg.keyPairBuilder().build() + assertNotNull pair + + PublicKey pub = pair.getPublic() + assert pub instanceof RSAPublicKey + assertEquals alg.preferredKeyBitLength, pub.modulus.bitLength() + + PrivateKey priv = pair.getPrivate() + assert priv instanceof RSAPrivateKey + assertEquals alg.preferredKeyBitLength, priv.modulus.bitLength() + + } else if (alg instanceof EdSignatureAlgorithm) { + + def pair = alg.keyPairBuilder().build() + assertNotNull pair + + PublicKey pub = pair.getPublic() + assert pub instanceof PublicKey + assertTrue EdwardsCurve.isEdwards(pub) + + PrivateKey priv = pair.getPrivate() + assert priv instanceof PrivateKey + assertTrue EdwardsCurve.isEdwards(priv) + + } else if (alg instanceof EcSignatureAlgorithm) { + + def pair = alg.keyPairBuilder().build() + assertNotNull pair + + int len = alg.orderBitLength + String asn1oid = "secp${len}r1" + String suffix = len == 256 ? ", X9.62 prime${len}v1" : '' + //the JDK only adds this extra suffix to the secp256r1 curve name and not secp384r1 or secp521r1 curve names + String jdkParamName = "$asn1oid [NIST P-${len}${suffix}]" as String + + PublicKey pub = pair.getPublic() + assert pub instanceof ECPublicKey + assertEquals "EC", pub.algorithm + if (pub.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, pub.params.name + } else { // JDK >= 15 + assertEquals asn1oid, pub.params.nameAndAliases[0] + } + assertEquals alg.orderBitLength, pub.params.order.bitLength() + + PrivateKey priv = pair.getPrivate() + assert priv instanceof ECPrivateKey + assertEquals "EC", priv.algorithm + if (priv.params.hasProperty('name')) { // JDK <= 14 + assertEquals jdkParamName, priv.params.name + } else { // JDK >= 15 + assertEquals asn1oid, priv.params.nameAndAliases[0] + } + assertEquals alg.orderBitLength, priv.params.order.bitLength() + + } else { + assertFalse alg instanceof SignatureAlgorithm + //assert we've accounted for all asymmetric ones above + } + } + } + + @Test + void testForPassword() { + def password = "whatever".toCharArray() + Password key = Keys.forPassword(password) + assertArrayEquals password, key.toCharArray() + assertTrue key instanceof PasswordSpec + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/StandardSecureDigestAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/StandardSecureDigestAlgorithmsTest.groovy new file mode 100644 index 00000000..e47f80be --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/security/StandardSecureDigestAlgorithmsTest.groovy @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 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.security + +import io.jsonwebtoken.Jwts +import org.junit.Test + +import static org.junit.Assert.* + +class StandardSecureDigestAlgorithmsTest { + + @Test + void testGet() { + for (SecureDigestAlgorithm alg : Jwts.SIG.values()) { + assertSame alg, Jwts.SIG.get(alg.getId()) + } + } + + @Test + void testGetCaseInsensitive() { + for (SecureDigestAlgorithm alg : Jwts.SIG.values()) { + assertSame alg, Jwts.SIG.get(alg.getId().toLowerCase()) + } + } + + @Test(expected = IllegalArgumentException) + void testGetWithInvalidId() { + //unlike the 'find' paradigm, 'for' requires the value to exist + Jwts.SIG.get('invalid') + } + + @Test + void testFindById() { + for (SecureDigestAlgorithm alg : Jwts.SIG.values()) { + assertSame alg, Jwts.SIG.find(alg.getId()) + } + } + + @Test + void testFindByIdCaseInsensitive() { + for (SecureDigestAlgorithm alg : Jwts.SIG.values()) { + assertSame alg, Jwts.SIG.find(alg.getId().toLowerCase()) + } + } + + @Test + void testFindByIdWithInvalidId() { + // 'find' paradigm can return null if not found + assertNull Jwts.SIG.find('invalid') + } + + @Test + void testFindEd448() { + assertNotNull Jwts.SIG.find('Ed448') + } + + @Test + void testFindEd448CaseInsensitive() { + assertNotNull Jwts.SIG.find('ED448') + assertNotNull Jwts.SIG.find('ed448') + } + + @Test + void testFindEd25519() { + assertNotNull Jwts.SIG.find('Ed25519') + } + + @Test + void testFindEd25519CaseInsensitive() { + assertNotNull Jwts.SIG.find('ED25519') + assertNotNull Jwts.SIG.find('ed25519') + } +} diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.crt.pem deleted file mode 120000 index 9f0f221c..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.key.pem deleted file mode 120000 index dbed23bf..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS256.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.crt.pem deleted file mode 120000 index 2b21f33d..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.key.pem deleted file mode 120000 index 8e6d4489..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS384.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.crt.pem deleted file mode 120000 index aad9991b..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.key.pem deleted file mode 120000 index 3ec40cc7..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/PS512.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/README.md b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/README.md deleted file mode 100644 index a89ac9d4..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/README.md +++ /dev/null @@ -1,14 +0,0 @@ -The `*.key.pem` and `*.crt.pem` files in this directory were created for testing as follows: - - openssl req -x509 -newkey rsa:2048 -keyout rsa2048.key.pem -out rsa2048.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' - openssl req -x509 -newkey rsa:3072 -keyout rsa3072.key.pem -out rsa3072.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' - openssl req -x509 -newkey rsa:4096 -keyout rsa4096.key.pem -out rsa4096.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' - -The only difference is the key size and file names using sizes of `2048`, `3072`, and `4096`. - -Each command creates a (non-password-protected) private key and a self-signed certificate for the associated key size, -valid for 1000 years. These files are intended for testing purposes only and shouldn't be used in a production system. - -Finally, the `RS*` and `PS*` files in this directory are just are symlinks back to these files based on the JWT alg -names and their respective key sizes. This enables easy file lookup based on the `SignatureAlgorithm` `name()` value -when authoring tests. diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.crt.pem deleted file mode 120000 index 9f0f221c..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.key.pem deleted file mode 120000 index dbed23bf..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS256.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa2048.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.crt.pem deleted file mode 120000 index 2b21f33d..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.key.pem deleted file mode 120000 index 8e6d4489..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS384.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa3072.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.crt.pem deleted file mode 120000 index aad9991b..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.crt.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.key.pem deleted file mode 120000 index 3ec40cc7..00000000 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/RS512.key.pem +++ /dev/null @@ -1 +0,0 @@ -rsa4096.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem new file mode 100644 index 00000000..1ad8fa25 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.crt.pem @@ -0,0 +1,28 @@ +# +# Copyright © 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. +# + +-----BEGIN CERTIFICATE----- +MIIBuDCCAV4CCQDHcF3Ya5gnbzAKBggqhkjOPQQDAjBjMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYG +A1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIxMTAxNDAw +MjAyOVoYDzMwMjExMDIyMDAyMDI5WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwPanNv +bndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAExNKMMIsawShLG4LYxpNP0gqdgK/K69UXCLt3AE3zp+T+/NDKZW0DtEdF +N8FZmjvmbE+AOQTyDtt0cjeyJK4k6TAKBggqhkjOPQQDAgNIADBFAiEAwU+JjD3a +xA8y5YvEuAx81/CrY+ioA7G0DwM4BdzpEEoCIHfKcVbJk4gLIABgVgulrmGhZZkU +/VnbQ/lGBN9qdwDg +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem new file mode 100644 index 00000000..451f7745 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.key.pem @@ -0,0 +1,21 @@ +# +# Copyright © 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. +# + +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICDbTYFnhiAEP1iipM9zKG+NoM4lbrteq54yz958Lc09oAoGCCqGSM49 +AwEHoUQDQgAExNKMMIsawShLG4LYxpNP0gqdgK/K69UXCLt3AE3zp+T+/NDKZW0D +tEdFN8FZmjvmbE+AOQTyDtt0cjeyJK4k6Q== +-----END EC PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.pub.pem new file mode 100644 index 00000000..6890279c --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES256.pub.pem @@ -0,0 +1,20 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExNKMMIsawShLG4LYxpNP0gqdgK/K +69UXCLt3AE3zp+T+/NDKZW0DtEdFN8FZmjvmbE+AOQTyDtt0cjeyJK4k6Q== +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem new file mode 100644 index 00000000..f73dd05e --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.crt.pem @@ -0,0 +1,29 @@ +# +# Copyright © 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. +# + +-----BEGIN CERTIFICATE----- +MIIB9jCCAXsCCQDMm4Wfx8vVCjAKBggqhkjOPQQDAjBjMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYG +A1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIxMTAxNDAw +MjIwNVoYDzMwMjExMDIyMDAyMjA1WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwPanNv +bndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEmIdXgmx2I4IQDi23KUjX7xJ4O5tMRj8QUh0hpJ5toVooZPBe0yINQ+aJHrE7 +snWjsTBFqFtAqodyage8G+GtkC6dwsffE2CWOi5Z+EMYInZk2W+iWNO69cGZ/uyH +9IVhMAoGCCqGSM49BAMCA2kAMGYCMQCbd1/Un8u2Rzv7Gr4sQtGGZuaZdeiQpQ4f +FqZHrLLte130JJ3rRjkuI8hDN38wc68CMQCJuBvGEg+vk1syB4jMO+O92h15nGUG +908CdAjnd9gA0xw71euP0nUFDGkoO4kmrF0= +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem new file mode 100644 index 00000000..3be54aae --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.key.pem @@ -0,0 +1,22 @@ +# +# Copyright © 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. +# + +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAwj/7H2g+1fvBRQ598Zna6rOXYfN/Kic9HGUSgFZizjDnIDEoFYNX4 +8fwBawFwSk2gBwYFK4EEACKhZANiAASYh1eCbHYjghAOLbcpSNfvEng7m0xGPxBS +HSGknm2hWihk8F7TIg1D5okesTuydaOxMEWoW0Cqh3JqB7wb4a2QLp3Cx98TYJY6 +Lln4QxgidmTZb6JY07r1wZn+7If0hWE= +-----END EC PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.pub.pem new file mode 100644 index 00000000..f62540e9 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES384.pub.pem @@ -0,0 +1,21 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmIdXgmx2I4IQDi23KUjX7xJ4O5tMRj8Q +Uh0hpJ5toVooZPBe0yINQ+aJHrE7snWjsTBFqFtAqodyage8G+GtkC6dwsffE2CW +Oi5Z+EMYInZk2W+iWNO69cGZ/uyH9IVh +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem new file mode 100644 index 00000000..f19e0cb6 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.crt.pem @@ -0,0 +1,31 @@ +# +# Copyright © 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. +# + +-----BEGIN CERTIFICATE----- +MIICPzCCAaECCQCKEqPphDTnFzAKBggqhkjOPQQDAjBjMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYG +A1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIxMTAxNDAw +MjIxOFoYDzMwMjExMDIyMDAyMjE4WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwPanNv +bndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIGbMBAGByqGSM49AgEGBSuBBAAj +A4GGAAQBS8o7xRLNRNsD5zlcw8fblmEkmHeNHHiuPDvRSLzx/CyaA5VACiMqg2Kx +9JnlbaUKa/R4y4lyuDMY9r922zp7tnEBWEiIKZqKaA0S9T+7/C6rEu2CL6p/DyWb +T/3CxAXZisNPwvqZei7bG/UoD3uCPRs8d4Rm9oJBJ9nfZJS/EuSWaC0wCgYIKoZI +zj0EAwIDgYsAMIGHAkEntHNUKUmgOamlKN5sqrXuJJnWClTpJ6XY/zIzmgRfOAyn +o//sToH5y019Fc3vkVVjRnWykdGwgK6fGYn7Q9oYMAJCAYhsiJGFYgzumbUNlADU +mMBAoF/g3e3QhFngu3i60lm7eiHmqGAIlGr36My2vJT1yfBQZR+54+7ZCZh3IClu +6wbY +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem new file mode 100644 index 00000000..457b5abd --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.key.pem @@ -0,0 +1,23 @@ +# +# Copyright © 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. +# + +-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEFRWZiTr6EKKQyuNJrR6zXT9KDtqQFm2Mi89fQcHGBC0GykIM8cUSyg +89/Vct6uaBlSATT2y4kXJ4tiDBX4A4nLaKAHBgUrgQQAI6GBiQOBhgAEAUvKO8US +zUTbA+c5XMPH25ZhJJh3jRx4rjw70Ui88fwsmgOVQAojKoNisfSZ5W2lCmv0eMuJ +crgzGPa/dts6e7ZxAVhIiCmaimgNEvU/u/wuqxLtgi+qfw8lm0/9wsQF2YrDT8L6 +mXou2xv1KA97gj0bPHeEZvaCQSfZ32SUvxLklmgt +-----END EC PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.pub.pem new file mode 100644 index 00000000..f814f1b6 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/ES512.pub.pem @@ -0,0 +1,22 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBS8o7xRLNRNsD5zlcw8fblmEkmHeN +HHiuPDvRSLzx/CyaA5VACiMqg2Kx9JnlbaUKa/R4y4lyuDMY9r922zp7tnEBWEiI +KZqKaA0S9T+7/C6rEu2CL6p/DyWbT/3CxAXZisNPwvqZei7bG/UoD3uCPRs8d4Rm +9oJBJ9nfZJS/EuSWaC0= +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.crt.pem new file mode 100644 index 00000000..6a923ea5 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.crt.pem @@ -0,0 +1,29 @@ +# +# 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. +# + +-----BEGIN CERTIFICATE----- +MIIB3TCCAY+gAwIBAgIUR0SYth9EKUux5/AgKexXgDJ/zY4wBQYDK2VwMGMxCzAJ +BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJh +bmNpc2NvMRgwFgYDVQQKDA9qc29ud2VidG9rZW4uaW8xDTALBgNVBAsMBGpqd3Qw +IBcNMjMwMTExMDM0MjE0WhgPMzAyMzAxMTkwMzQyMTRaMGMxCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKDA9qc29ud2VidG9rZW4uaW8xDTALBgNVBAsMBGpqd3QwKjAFBgMrZXAD +IQDPuZvEZhWrWb1OfF+VVvdWMcBKDOcoEWoJAF4l0mXAlKNTMFEwHQYDVR0OBBYE +FLgHHuBTan5H+eyXqorf9qgRxTIXMB8GA1UdIwQYMBaAFLgHHuBTan5H+eyXqorf +9qgRxTIXMA8GA1UdEwEB/wQFMAMBAf8wBQYDK2VwA0EAbTTvZlGcKhLdP17vYjNs +Q5iHj0C3N8w3zLU7CN0Dd9f5WahLQW4wJfL84eLoLXxoHqyReDPXnGBxaC8Zx8/F +Ag== +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.key.pem new file mode 100644 index 00000000..3a7b1937 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.key.pem @@ -0,0 +1,19 @@ +# +# 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. +# + +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIN9YgXIwPD/R319mdGGHAPvfKcoJriMauwV6NoopfUOf +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.pub.pem new file mode 100644 index 00000000..17ef3f90 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed25519.pub.pem @@ -0,0 +1,19 @@ +# +# 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. +# + +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAz7mbxGYVq1m9TnxflVb3VjHASgznKBFqCQBeJdJlwJQ= +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.crt.pem new file mode 100644 index 00000000..1ef4cb8a --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.crt.pem @@ -0,0 +1,30 @@ +# +# 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. +# + +-----BEGIN CERTIFICATE----- +MIICKDCCAaigAwIBAgIUD+SddglKQqU+prMvjU7UNc/MbuowBQYDK2VxMGMxCzAJ +BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJh +bmNpc2NvMRgwFgYDVQQKDA9qc29ud2VidG9rZW4uaW8xDTALBgNVBAsMBGpqd3Qw +IBcNMjMwMTExMDM0NDEyWhgPMzAyMzAxMTkwMzQ0MTJaMGMxCzAJBgNVBAYTAlVT +MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRgw +FgYDVQQKDA9qc29ud2VidG9rZW4uaW8xDTALBgNVBAsMBGpqd3QwQzAFBgMrZXED +OgBlMnl5cksy+vsGViOV1bUBU2JxQr8GSJcFXBcSW4c75st1WFJpEfwCFUpN8IQD +uqErjZtG2lOtYoCjUzBRMB0GA1UdDgQWBBQnchTx8D04u4HFyvoAJ7BjpamCJjAf +BgNVHSMEGDAWgBQnchTx8D04u4HFyvoAJ7BjpamCJjAPBgNVHRMBAf8EBTADAQH/ +MAUGAytlcQNzADnVBrSlzO0WH5lXiEtJ+XEQbZft2ky/T44/QF4cVO0Ucn5rVfxV +N6MjqvsYZOKiFFujp5foB3QpgH5OtX/eCVi8lmlA2OLpQH2R/8aHn7uikGGGY1k1 +C4vJoRsMPRt5EVGLHzoK4WZLKwquF9x011svAA== +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.key.pem new file mode 100644 index 00000000..2cb86f3d --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.key.pem @@ -0,0 +1,20 @@ +# +# 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. +# + +-----BEGIN PRIVATE KEY----- +MEcCAQAwBQYDK2VxBDsEOad9P+Hd/6/J81KL3EoBbe5s4ij7yQaYavEiRBwtEBhQ +j2Mn5JUQxQYH9LS1uBP8SsDzx3T/vHciNQ== +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.pub.pem new file mode 100644 index 00000000..aea427f2 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/Ed448.pub.pem @@ -0,0 +1,20 @@ +# +# 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. +# + +-----BEGIN PUBLIC KEY----- +MEMwBQYDK2VxAzoAZTJ5eXJLMvr7BlYjldW1AVNicUK/BkiXBVwXEluHO+bLdVhS +aRH8AhVKTfCEA7qhK42bRtpTrWKA +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.crt.pem new file mode 120000 index 00000000..a297dd34 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.crt.pem @@ -0,0 +1 @@ +Ed448.crt.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.key.pem new file mode 120000 index 00000000..e2381b1f --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.key.pem @@ -0,0 +1 @@ +Ed448.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.pub.pem new file mode 120000 index 00000000..b8daf2c0 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/EdDSA.pub.pem @@ -0,0 +1 @@ +Ed448.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem new file mode 100644 index 00000000..45195339 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.crt.pem @@ -0,0 +1,36 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN CERTIFICATE----- +MIIDRDCCAiwCCQCgd9OzR40NCDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQzM1oYDzMwMjAwMjExMjMwNDMzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzkH0MwxQ2cUFWsvOPVFqI/dk2EFTjQolCy97mI5/wYCb +aOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ/XCaybBlp61CLez7dQ +2h5jUFEJ6FJcjeKHS+MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi +0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4+5+ +7MkC33+8+neZUzS7b6NdFxh6T/pMXpkf8d81fzVo4ZBMloweW0/l8MOdVxeX7M/7 +XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8QIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQBGbfmJumXEHMLko1ioY/eY5EYgrBRJAuuAMGqBZmK+1Iy2CqB90aEh +ve+jXjIBsrvXRuLxMdlzoP58Ia9C5M+78Vq0bEjuGJu3zxGev11Gt4E3V6bWfT7G +fhg66dbmjnqkhgSzpDzfYR7HHOQiDAGe5IH5FbvWehRzENoAODHHP1z3NdoGhsl9 +4DIjOTGYdhW0yUTSjGTWygo6OPU2L4M2k0gTA06FkvdLIS450GWRpgoVO/vfcPnO +h8KwZcWVwJVmG0Hv0fNhQk/tRuhYhCWGxc7gxkbLb7/xPpPKMD6EvgG0BSm27NxO +H5l3KYwtbdj5nYHU73cLqC1D6ki6F8+h +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem new file mode 100644 index 00000000..61088c2f --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.key.pem @@ -0,0 +1,44 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOQfQzDFDZxQVa +y849UWoj92TYQVONCiULL3uYjn/BgJto6hn1GbtzrvmYB56ZG03OA1UTPubb3wQ2 +o0ao+TY2/ekEn9cJrJsGWnrUIt7Pt1DaHmNQUQnoUlyN4odL4zBevnq3YhJ18s0x +i1UjGO9dBxjHlWZLiYipPGPmBUa1BWLQZd0fpK+l0pXR+MT0o6GOx0F8pDNl9xCW +6weDylOHC+OkzAJmeTEJLREpr3Lj7n7syQLff7z6d5lTNLtvo10XGHpP+kxemR/x +3zV/NWjhkEyWjB5bT+Xww51XF5fsz/tdILVqeTmLcgRlyi0uYlgzASjusyllUt68 +RDXXwSjxAgMBAAECggEAZ90ahaJMDH2ERsaeoo4e7uGjrKqo0jsrkEhm6tnHR7/l +gp1wWNaOaKDSG1aq7NqtAXL4Imroggv56TGrYWetf1+5OZTsCnkaz8Y8WBr/LIZZ +dp0a0dUdMhpXdTN/gh1zvCIbVcFTHoYYAjzxsGzcDHKIbeizzJIDeYVpoOlDQ9/9 +Bv6ft4mhaG5SHVnec9QdmbJnKDq5rI4aPXCCXOCzDjdTVfgntdH5TvoCH91ESSKw +kddciAbVsXoOWnBx3jKMj+hIA4F1p6nzZUbiVzmxhqfShQhDnCEvq8tF7KqRbUsS +Gx8MVtwSkEGaiJCDVjwSRGkghXlguNwZcfnWMtGMYQKBgQDmFWAApeXv4xXF2a/1 +HKumO5Z+w+XkKiM76YyTHTKO/KtDYRJiIlJMgx+hoRTBwlpYDrlbS9+Jnm7bZ9Ib +pxRyMAFRoV7eIhnoAn9KrxhS8xCYF2Km7U1lg/+m3pFKghjV4+K1GHbggmvoiIY1 +2t250zkZSslwTxu/2+jRKYOptQKBgQDlfYrzvuGqClJ9QClxlOV2UrWiGxq6eTgL +4V3l0HwPU9OW/hX0Hued8S70Dpb3o+AAyptbcAqFjSdyIPMbCfKLQQkKpfBUtOvb +Nm12z/VNKNZbu7kvaOJHunQNHzyMEHcjsB9daAVI0gJZKN+m6Qh4VF4jao7G9GNR +d7ge0KcXzQKBgQCqf8p9kHJ9OsVmsTMgK1fTvrJ+S8LvOn6TpjVCy08tAHYVXzjV +OePMyRpGluyfzNtQB9E5o1cKTzqNIjljvoN7PrGrgS6g45pZAIi9mlUnGvIAEsxL +MOy6vn9Tc/kswo2O6umUE4X8RwmZ7pmuDPtj+e+FG5N8w1Kn8VlsrhvgRQKBgAgz +clG/koTnFYeQUWrTrVeLIR6H5W6gglY6WYaq6qQJlNgigFpW+GP2iH0EQHTdEFY2 +51JfMKERKEW107o1ostDKbWNtIbyaDNPQJ4sVFHLkc15aea90shJa3hEk39V30wR +MS2/V+EAUEErasKmNT1Hlo2hczS86wewRY4kWrRJAoGAeYUG04cu55GwCgp50P3J +0NCNyiOkhnaj0wGPztMbDqNkaUAoaycoEsas5lhRAWT4YIVglz5pwR4uiI57w1cL +Mvjk5yDiQs7h3bV/qtm95YPBBC+y3mmZYlEA1lH0qktRNBlMVtfYkPztBh50UBOH +8qhIwqrpm3+JJ1p2p0XPl1c= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.pkcs1.key.pem new file mode 120000 index 00000000..6aba8b0c --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.pkcs1.key.pem @@ -0,0 +1 @@ +rsa2048.pkcs1.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.pub.pem new file mode 120000 index 00000000..518164eb --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS256.pub.pem @@ -0,0 +1 @@ +rsa2048.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem new file mode 100644 index 00000000..9f6e42e3 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.crt.pem @@ -0,0 +1,41 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN CERTIFICATE----- +MIIERDCCAqwCCQDqxucO41yAmTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQ1OFoYDzMwMjAwMjExMjMwNDU4WjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBojANBgkqhkiG9w0BAQEF +AAOCAY8AMIIBigKCAYEA0DmQS4Xtgu5xtnQdxkuzB1j+W4OvNEOVOsg3Zcn9W1d5 +NowtngUh9K9vwBpl59M2j5PHj9dseEIuRqr++ZnZhBMlh/lLiIQ0oQ3cEa7wrwJO +i9ycZZbeNDHVNzAdQZEQR1DIMUhSTEMR96pVD4a9DzBKLQJaKxZRUOrMhf3QhAZ5 +9m0d/Kqu1Dm6YeWMLQQEewAaSQ9g22gty1EcLAvp/vnhR/DJSYIHCayd5mZwvk9q +WkXySfUmJGP70GFGG4GOnVLLkmCQgfT+OrzTtiIzNT26mtAsoUMnoD42TTBkR0aL +hULcj1MYUcB9uVCmJTvYuiCTODoNlL8T40r9L99HoHlTWVi9vf6I1vyNdHp3dXKB +MVL13+i5BeNIjADr0KKs0jtEEicWyuVhJz3rPLzBbPhz/DGQ/hTj/DRSdo9L9YA4 +WkqgD8uFUvIEKAJ/hXYx3QPEiIMU7hT4jk2Z2SIBiKKiNW4E/20EZOFmaeNWQaqq +mwX0cHtMygJtTYnEJ1IDAgMBAAEwDQYJKoZIhvcNAQELBQADggGBAGKkmv6d372z +Ujt1qjsjH7LHfIsPXdvnp7OhvujDEtY7dzwDCtR40zgB2qp+iXUO61FXErx8yDp9 +l7sDzk0AjY7RupANuo/3FyDuo0WoTUV3CJNnXf3Mrvu/DMjbaS6D4Jryz/HLE+2r +GYtdm165FZK/hQXuFfurkc4yqjrX90Wr+YHeen2y5Wk3jeUknmdp97F6+zkq6N5D +dKjy/ZOvy+1huNd5bzvJoiZLKqdSh/RQUoU6AP1p+83lo+7cPvS/zm/HvwxwMamA +1Cip1FypNxUxt5HR4bC5LwEvMTZ/+UTEelbyfjMdYU97aa58nPoMxf7DRBbr0tfj +GItI+mMoAw60eIaDbTncXvO1LVrFF5BfzVOTQ8ioPRwI7A5LMSC5JvxW8KsW2VX0 +vGwRbw8I6HXGRbBZ3zwmAK73q7go31+Dl/5VPFo+fVTL0P7/k/g0ZAtCu4/Wly9e +DLnYMoZbIF5lgp9cAzPOaWXiInsA6HSdgFUfXsBemRpholuw+Sacxg== +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem new file mode 100644 index 00000000..4fd9098b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.key.pem @@ -0,0 +1,56 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN PRIVATE KEY----- +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDQOZBLhe2C7nG2 +dB3GS7MHWP5bg680Q5U6yDdlyf1bV3k2jC2eBSH0r2/AGmXn0zaPk8eP12x4Qi5G +qv75mdmEEyWH+UuIhDShDdwRrvCvAk6L3Jxllt40MdU3MB1BkRBHUMgxSFJMQxH3 +qlUPhr0PMEotAlorFlFQ6syF/dCEBnn2bR38qq7UObph5YwtBAR7ABpJD2DbaC3L +URwsC+n++eFH8MlJggcJrJ3mZnC+T2paRfJJ9SYkY/vQYUYbgY6dUsuSYJCB9P46 +vNO2IjM1Pbqa0CyhQyegPjZNMGRHRouFQtyPUxhRwH25UKYlO9i6IJM4Og2UvxPj +Sv0v30egeVNZWL29/ojW/I10end1coExUvXf6LkF40iMAOvQoqzSO0QSJxbK5WEn +Pes8vMFs+HP8MZD+FOP8NFJ2j0v1gDhaSqAPy4VS8gQoAn+FdjHdA8SIgxTuFPiO +TZnZIgGIoqI1bgT/bQRk4WZp41ZBqqqbBfRwe0zKAm1NicQnUgMCAwEAAQKCAYAg +ewo+LasKBIXqbxyB5ScNG126CsWWwoARxk+V6jdCO1fmIWGwR56vW3p0HeoNio31 +QZkcn/8El1Y+ocfaSZx7lL0DA+k7Z1wKT24nuAFFW3fDK2ueETWiMK/QxwmZQ7al +WT2RKnXj/YZc+s3/+QWey+qWMMq98+JFXAsBT8FqBtSZkxXdZwaUhljDkpoWH41P +Xom7IdH7B7o0//cEC+u5YWM55J6Rf933LV0IJqypkxvE7ypHTR1hCdOrArF78u5z +Jg61hZRDi9t+X2RNZZ027ysrVLU/gre6XzSZI1a7NygDOSWBmcycQBAf6ZYJDdeb +mLy5M62K0fNavaxiuspA/WD3k4BsXSsK/rGNU6DvpeuymEbWFzPIoD5uKWTwHdSa +5ZrJGcR+Q5D12EersJi3jm52tYqYE91sJ8x+q6Ko+u7kWSbUCssqJLITdCqBdoEL +tpZspCzfCShJ+7CqlC1jEAIRdYFWFgIk76eLyr1k8aYI+NBqwfQbTzNK9Okj07kC +gcEA6BSD2iW2KEHyPi10BsqiWLKWCS6e5UjVBZEgD7+c6pYABxvXrMCKgseyd4LY +FBJ15MLNp3KS1vozlQEYp7LFAhpNeYMADql39ZNc7FQPcv+QsyQfDLP26eypabhN +BDexMcBY4jhZNkEBXjdxU9l0rGCQw82qLO5mK4WuKfyj+IX0iQv6BOzfBTKYNsgt +JAb289KeyrsV7rAoxxxfmsjYqsQadeCaOMQfkATAKyVaMrs6aJfJokuz5ibv+PRz +p6JdAoHBAOWvnzNNUF5BAanmk6BeiYK1tf9xAjJ5tAOAdqqflbDVBlZVE8qGYOH2 +J7x2/LQVz+Dm3chC5AdUL0tu5qZ+rAr8Vc7lJDvGkbcfTaEv4/VbF1gyDwi+dwn1 +MV3WQMEuFrqLDa1G3zxYER6PsO1DcwMTMRiWWlF6F77FnRm1zmleIkeXEOPW4a3J +Id2W043od/tbNr0j4sU/Ha3M+Eb91XjkSVulsABL+98CP/BnqWEFQABgQmfBsXMD +Kla06hw/3wKBwHm7iQ28CjhDnxUOMnX9g/qScjCOy7no4hPxc6fPEjfaRll0OUTc +GctPhEU71Ktyo3RC2iyi5HLu+m+GC7CrDLt1oH3EQRtvuQSPL4am8ROZCgVtRPwc +yb8Z7CMQERXNQJygD/9ZHzJeFqGc40zgG1rvq/+IuWKoCd96V0iexENvwDzCk3pR +5QmM6FqT1Vm4bYCnUbN1PqPcswb90wgVodCw3FBIZ5yvAv8//qyjAxTpMFH8jD8d +BlgKxIUJdEDR4QKBwGZapfJBsN/fzjL9aqobluHlwg3sOVNvArZQyBDu/tEHjURp +s2EcEw5/GGQXDjPeSH3rw8ebb2yIqm7OJADsEBTxL/f8CvKMYaEeVQTQh6BuEHAg +Fq0J25hXaMFtWfv8YuqMTvL50z9b630YAXsqBJXJNqbDUcpfQzejbofnie1QoqwO +eNtfhcBhEjNiJDJn9xfPJQySclr97mbmIXnZYgj2im5J3q2zLrHJmd6zAzsWENha +DR2Zpk8fiP2Mr4sZNwKBwQDX2Y/Ycr/IQtNdP/YsFIDeNHJ+dBLDM4JJCetlbzsY +poIKM9+ZvC9EST0KoEumhUT4Fy75b75nbzRGRDmFDNyOxRcHixFVnbgZqWyAeCbw +xNCKrIbtrXk5JvFy5y0yjMdBeB2uB1KJZhesuwUS1JnhA8RDapQ7ZwpoleRd+iqg +3RJTtcvo5Ky6vdz74isxBL8WH+PMqQEm1el8Jwix5dHx5mKH5QM2XnkUm78V/NX9 +5I2wbxUhb3FO7gj9pxJbwX4= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.pkcs1.key.pem new file mode 120000 index 00000000..75de2b26 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.pkcs1.key.pem @@ -0,0 +1 @@ +rsa3072.pkcs1.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.pub.pem new file mode 120000 index 00000000..6d0de80e --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS384.pub.pem @@ -0,0 +1 @@ +rsa3072.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem new file mode 100644 index 00000000..55a08c37 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.crt.pem @@ -0,0 +1,47 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN CERTIFICATE----- +MIIFRDCCAywCCQC4g2isVGolKjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDY1MloYDzMwMjAwMjExMjMwNjUyWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAw7jTXeRwCRrDdYnrIwcLvSNfhpmJZ0ap1jzKVgUyCOYL +TaB9+naJRjHqx7B5wgx/ArRF2nluQ5tawZPMFw2I/iqXYrQEPTbq1XVugYC/C491 +bcXOTKx+DgEvnhNysm/KmzFsEcw78prB5sIAZSR+S5zZPuny4zww2UzE9TZ433RB +kyA+wVkd64bgXdkMrVc+gsRsOtvwPFbQ89zg8d/pNV0mDtjDsfiYw0pSAah11fJG +a+aRc46CqFu/6rHuN4uq6542LdtshPbHz29VKHxk8agtcx06+8F05Bg4LFm1rRhY +0g3KsT7s8XHMVdo9h2bIQuWOaFg3mehpH6ZYBV4ENo98V/jDaUPpBHsaUXw4fG/w +rnI+YwRjGlmp2QEr5VRfh0x8Addf6N64lmbQUpCPhJweJd54D3JvIpJr8HiG3GYW +eFsrmDzmhrozZHxE4P7UesW6lWwQzGfwYGXs7j6TEa2hZ8EB/t1jsYNjZ5UYY/Jb +KgFGSkMGje4Bi5Bv6kh4+pp3DT5QsG/AfLVlr5ineLDWkJ15uZjOxl12EOPXOCWV +iVqS6rayJfb95YJQ52rT4H83BsApHbzFj7q/CIaeJkUdv9GJ2SOADcXdgG7Xk7tH +qb1VIH4zRBo5mc0qN1cAwjopxHv2h3tGaTHKbptvfiLulH+AvvuWmLMEQo6ZDlsC +AwEAATANBgkqhkiG9w0BAQsFAAOCAgEApnHjMQwt5hm6UlEDvdWCYbh7ctkLbgwR +iBP1lvunm2oF0jGpipt8oDR/TT43usb6ieuU+ABksjxOROeoVZbK8bEpnzeo3nNE +41ERI3Byjp7tsja8QGG0uBk9QZ0+7MhJqhEDVAIbS0Lf4exkWiLZrW7ogAEFYKTN +DE6CxOcfR/kXj6ejuCnvN4xYqnw8G/OF/3tnHMfKnnnqtMmWdAKd3Y5S1EJZ5vtp +lZ3I9HA5Hx0sTH1ruCOIRzaC5En1c6zW1HjxmeAqLeG814gezlEhHzb4SCkabgQh +Bq15O8eQaW92f8xZoUQN25w7SNYszk9AdhroJz3+BOzG3+Y1EInLk5hDHT8oUNFz +e8EosJEwJDK3wq9YOhn8PUT/DacyNKONJVNly3fTBXoSR3oReW61p6T19z4AYsY9 +qMwSjIL2UcgAF8Kpsx2NdQrDfdveNMhul7AjIgz+e2DtRqCkZ6ypdhht2pmlpiXO +TiUG/1OBq2yTeJF9LjAUzsSNnsZ/F8pJbwSpr7VqDmTNGTfrh6x1ojHNFjJeTqK8 +MCTmQtJJTAbV4nuB+thFFWDx0IWvbG7ViYds9sdJNO4L3baXeAioJhHs5buBy3eb +ZWjLAwHpSCqNY3d6+ouGLwE1YVFsk8sV9UM+gl15VynKkunbYoKhiD82HGASNYtE +33eif1l5Nk0= +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem new file mode 100644 index 00000000..cb8296b2 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.key.pem @@ -0,0 +1,68 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDDuNNd5HAJGsN1 +iesjBwu9I1+GmYlnRqnWPMpWBTII5gtNoH36dolGMerHsHnCDH8CtEXaeW5Dm1rB +k8wXDYj+KpditAQ9NurVdW6BgL8Lj3Vtxc5MrH4OAS+eE3Kyb8qbMWwRzDvymsHm +wgBlJH5LnNk+6fLjPDDZTMT1NnjfdEGTID7BWR3rhuBd2QytVz6CxGw62/A8VtDz +3ODx3+k1XSYO2MOx+JjDSlIBqHXV8kZr5pFzjoKoW7/qse43i6rrnjYt22yE9sfP +b1UofGTxqC1zHTr7wXTkGDgsWbWtGFjSDcqxPuzxccxV2j2HZshC5Y5oWDeZ6Gkf +plgFXgQ2j3xX+MNpQ+kEexpRfDh8b/Cucj5jBGMaWanZASvlVF+HTHwB11/o3riW +ZtBSkI+EnB4l3ngPcm8ikmvweIbcZhZ4WyuYPOaGujNkfETg/tR6xbqVbBDMZ/Bg +ZezuPpMRraFnwQH+3WOxg2NnlRhj8lsqAUZKQwaN7gGLkG/qSHj6mncNPlCwb8B8 +tWWvmKd4sNaQnXm5mM7GXXYQ49c4JZWJWpLqtrIl9v3lglDnatPgfzcGwCkdvMWP +ur8Ihp4mRR2/0YnZI4ANxd2AbteTu0epvVUgfjNEGjmZzSo3VwDCOinEe/aHe0Zp +Mcpum29+Iu6Uf4C++5aYswRCjpkOWwIDAQABAoICACPSgUUvGV5hOqMZsiLAGGLu +xX4iPebcJRukFrh1zPmZ+TmlBUnBRlDFtB4Ga9KbbOe2zQ42qXrQRWUmwvT5Mjiq +3Phg0GHP2l1lV+t2AAGCqVCFIsQf0haIGwoIrzZ/hYqwGgKL6fD2aETu/xmD+2Wl +eJGuShlTG/G5vlbPOIJVieb+wN2sjPBdyFUE8/AKBtPyVYjUVn0EusvXgohinhF5 +UgznmbHKOVONF8Nb7O1SoZcAJWEMFVfxKwguttYNxyPG2k28WnlfnaSW0PRPCD6+ +tErcb75CYz2YPTfI15qt2RvhEFcumDl8xZR1FEvjAQZVc6Ife1W9FviG/pdE5Oox +lzsdOtIVSsrkDgl+kPmQczTdl8qWndh1c5rnOWALX388I+CWEgNDY0cfD6W2Xg6i +IIYCZ3mm0ZBZVTh1qCTciPFs6eBLZ2r/N+/dT0tTYrtKPKE8FfUqes9eFI9yEMmp +XKRw7tZZ78olS8eii1xiPsTSwNOoCFclyRzIE/Wfml2oAWkRiuC9tQZwkw6mj55p +5g1kxz0OtG+KrVaFxronaB1LLuNKJ41vRvmxevD6LnvGm/PMMkbizXGm7VPpaT9G +ETfNnk0ZKGSVemEmr+zrV2cAlAX7ZR+9ULY8DwjBaKO2g7w0ONqBdzuAXbP2T9WA +Zhmc3YiIgx1IdvH286YxAoIBAQDj2kJLqD8MOhTTNLwYQAc2WA8C1IxJWObShPNx +N2n7RQl7wL7gdphNQ4jbkbGEKqu4eJUlBBHPNUpTcaaYXRD0mNcGRXloXVSaVhLU +vXt6/hEiSk9TX6gGT8XGmQ0xfDZfT+yfrO+z6cABfMh1da8xKDYxg8lAok79jJRr +OWwzKsMj94LUOP7Z8feh4rjvrR0nHoSC4Ds8mrXOZTt42xMVunPxgHEf39Hx9Ikx +qiKvOZHdqRdru11xcD5AW6nvwgYKcTfvcKDFYXwfi0WMFvecSY9nK8Qg5ZMba0wI +pOlccoyat1EOy8aDCYr1OSmzhoQrCJGVTqyHDnce53FZQyQDAoIBAQDb5nM7Df11 +4JMDM0zbU90nDf+Qm6BXiHtLpADiTwf6NFLs26+u026Lo/60LxjHKif1UJPidma4 +b37Yeuwvlg2BHg+A1guYSv75k/bGIViGP3zDAWQRJ2/LonxBNcEGQToP7bBd5UM4 +dKCeWgU9PMF7qa1/xs5rrEAsqGNYrKu45Ng0YOm61NKL4zf5rUfEx/gX7v9NW/er +q9p0Ms5k1UK/AXaeMGhzT75vhoiMObMv2BKM/IAR0Wrm/xYCvnuey7hva+NRGOJ/ +gm9KnUxteafEqllA/VHgYpqZFEXwoR4Ty4ByBXDYTcLG5m7LynAfo5NUIHI3HB+v +uKV7hX3egJjJAoIBAQC3PVKth4vUqG0RAcr28Z8bPCwuSYLchctzqAojlb38niOn +S3X2DEolcNeCRSPut2ZMP2UqVKCB9Ehm3PJufAHjw3rBh2PA47XjPK9+OTgxzFs5 +KWusEDSPht31/iYXEt6jPiJ8s1Y+aRDJ4XFQzSjsLnuOzH4wJZfC3qiJpq92YsB2 +j1m+lGuYGLjejvfNgHn+eNN2cSASeBUX/F+crQonIkCWCoZvbM9pdxBSSZIFOxYs +ngzAzfiy/uKBXXZH49B5211xiTEyK1joAVgX9myBWsMh5JehIR9yIJMQLJejile7 +IQvmC0kFHsqKtcLsppRqC0URPykOoDp6NwT4FT/DAoIBAQDXmhtgy1a3PHjnqmSw +pokuwYrRPcT4DdjVUPeM6+/mYWbs1Hhr8OFyCFiyUXr5y1tiKp7Ua0JLkwXLOrpX +7cdP0SliKHs11lIoYeqSWB9zgMvSZoq2RvRVs/of9ZRLjahf9av2Y9KEh9TzbU+1 +utv5Y2O45DN/XmONZYwCZUn4/mb89Ag2JnRIs38uTbcQOQAGd03Zi1JJ/zUwuJ+k +PXQz0jt63fuLE6SjtEQtOGV3g2Ks2OS4k5s84N2z0w9holwy4pT97mgknL6BabiF +ncHgESVxku20EvmBHV91joLu5ZgKM0twyM0wNr5rERDd9IN++FEDt49ZurCFa1z9 +yxgBAoIBAC3HJzGb7Cufqw1JNng8H1mkJ5+1ZCNo7jy/aUYd5OacGTCNTcvuPTj+ +2iGvn4G0JR7pukhU5dVtGMQGpmmp8zk6/xzmyqeeiQNi4wdEgMALq4I0nynXkxDv +utKsXpmPiwyxmwCg9EY7AokfGWbxI5Yf7HkrjxME7jHz31lt5OF7AKyE1veFYWRa +puP1KVjNH7UAoE3WHnPnj7xvfQspXVRpzPWXH86XVonqnjQgu3SDkclPbkjg4HVj +athb6h5RN5bYx1cbUvo3JssBYl92FlXPU9lLzgv4nALUdVSi8PjbjQ7WXdxaKPdf +lczRTJNTE/KNUE0pkC5P4c/e0A1OFu0= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.pkcs1.key.pem new file mode 120000 index 00000000..15d6f5c0 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.pkcs1.key.pem @@ -0,0 +1 @@ +rsa4096.pkcs1.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.pub.pem new file mode 120000 index 00000000..74c57d7f --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/PS512.pub.pem @@ -0,0 +1 @@ +rsa4096.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md b/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md new file mode 100644 index 00000000..81428ba8 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/README.md @@ -0,0 +1,68 @@ +The RSA `*.key.pem`, `*.crt.pem`, `*.pub.pem`, and `*.pkcs1.key.pem` files in this directory were created for testing as follows: + + openssl req -x509 -newkey rsa:2048 -keyout rsa2048.key.pem -out rsa2048.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -x509 -newkey rsa:3072 -keyout rsa3072.key.pem -out rsa3072.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -x509 -newkey rsa:4096 -keyout rsa4096.key.pem -out rsa4096.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + + # extract the public key from the X.509 certificates to their own files: + openssl x509 -pubkey -noout -in rsa2048.crt.pem > rsa2048.pub.pem + openssl x509 -pubkey -noout -in rsa3072.crt.pem > rsa3072.pub.pem + openssl x509 -pubkey -noout -in rsa4096.crt.pem > rsa4096.pub.pem + + # convert the PKCS8 private key format to PKCS1 format for additional testing: + openssl rsa -in rsa2048.key.pem -out rsa2048.pkcs1.key.pem + openssl rsa -in rsa3072.key.pem -out rsa3072.pkcs1.key.pem + openssl rsa -in rsa4096.key.pem -out rsa4096.pkcs1.key.pem + +The only difference is the key size and file names using sizes of `2048`, `3072`, and `4096`. + +The Elliptic Curve `*.key.pem`, `*.crt.pem` and `*.pub.pem` files in this directory were created for testing as follows: + + # prime256v1 is the ID that OpenSSL uses for secp256r1. It uses the other secp* IDs as expected: + openssl ecparam -name prime256v1 -genkey -noout -out ES256.key.pem + openssl ecparam -name secp384r1 -genkey -noout -out ES384.key.pem + openssl ecparam -name secp521r1 -genkey -noout -out ES512.key.pem + + # generate X.509 Certificates containing the public key based on the private key: + openssl req -new -x509 -key ES256.key.pem -out ES256.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -new -x509 -key ES384.key.pem -out ES384.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -new -x509 -key ES512.key.pem -out ES512.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + + # extract the public key from the X.509 certificates to their own files: + openssl x509 -pubkey -noout -in ES256.crt.pem > ES256.pub.pem + openssl x509 -pubkey -noout -in ES384.crt.pem > ES384.pub.pem + openssl x509 -pubkey -noout -in ES512.crt.pem > ES512.pub.pem + +The Edwards Curve `*.key.pem`, `*.crt.pem` and `*.pub.pem` files in this directory were created for testing as follows +Note that we don't/can't create self-signed certificates (`*.crt.pem` files) for X25519 and X448 because these +algorithms cannot be used for signing (perhaps we could have signed them with another key, but it wasn't necessary +for our testing): + + # generate the private keys: + openssl genpkey -algorithm Ed25519 -out Ed25519.key.pem + openssl genpkey -algorithm X25519 -out X25519.key.pem + openssl genpkey -algorithm Ed448 -out Ed448.key.pem + openssl genpkey -algorithm X448 -out X448.key.pem + + # obtain the public key from the private key: + openssl pkey -pubout -inform pem -outform pem -in X25519.key.pem -out X25519.pub.pem + openssl pkey -pubout -inform pem -outform pem -in X448.key.pem -out X448.pub.pem + openssl pkey -pubout -inform pem -outform pem -in Ed25519.key.pem -out Ed25519.pub.pem + openssl pkey -pubout -inform pem -outform pem -in Ed448.key.pem -out Ed448.pub.pem + + # generate X.509 Certificates containing the public key based on the private key: + openssl req -new -x509 -key Ed25519.key.pem -out Ed25519.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + openssl req -new -x509 -key Ed448.key.pem -out Ed448.crt.pem -days 365250 -nodes -subj '/C=US/ST=California/L=San Francisco/O=jsonwebtoken.io/OU=jjwt' + # note that there are no self-signed certificates for X25519 and X448 because these algorithms can't be used for signing + +The above commands create a (non-password-protected) private key and a self-signed certificate for the associated key +size, valid for 1000 years. These files are intended for testing purposes only and shouldn't be used in a production +system. + +All `ES*`, `RS*`, `PS*`, `X*` and `Ed*` file prefixes are equal to JWA standard `SignatureAlgorithm` IDs or Edwards +Curve IDs. This allows easy file lookup based on the `SignatureAlgorithm` `getId()` or `EdwardsCurve#getId()` value +when authoring tests. + +Finally, the `RS*`, `PS*`, and `EdDSA*` files in this directory are just are symlinks back to source files based on +the JWT alg names and their respective key sizes. This is so the `RS*` and `PS*` algorithms can use the same files +since there is no difference in keys between the two sets of algorithms. diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem new file mode 100644 index 00000000..45195339 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.crt.pem @@ -0,0 +1,36 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN CERTIFICATE----- +MIIDRDCCAiwCCQCgd9OzR40NCDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQzM1oYDzMwMjAwMjExMjMwNDMzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzkH0MwxQ2cUFWsvOPVFqI/dk2EFTjQolCy97mI5/wYCb +aOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ/XCaybBlp61CLez7dQ +2h5jUFEJ6FJcjeKHS+MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi +0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4+5+ +7MkC33+8+neZUzS7b6NdFxh6T/pMXpkf8d81fzVo4ZBMloweW0/l8MOdVxeX7M/7 +XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8QIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQBGbfmJumXEHMLko1ioY/eY5EYgrBRJAuuAMGqBZmK+1Iy2CqB90aEh +ve+jXjIBsrvXRuLxMdlzoP58Ia9C5M+78Vq0bEjuGJu3zxGev11Gt4E3V6bWfT7G +fhg66dbmjnqkhgSzpDzfYR7HHOQiDAGe5IH5FbvWehRzENoAODHHP1z3NdoGhsl9 +4DIjOTGYdhW0yUTSjGTWygo6OPU2L4M2k0gTA06FkvdLIS450GWRpgoVO/vfcPnO +h8KwZcWVwJVmG0Hv0fNhQk/tRuhYhCWGxc7gxkbLb7/xPpPKMD6EvgG0BSm27NxO +H5l3KYwtbdj5nYHU73cLqC1D6ki6F8+h +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem new file mode 100644 index 00000000..61088c2f --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.key.pem @@ -0,0 +1,44 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOQfQzDFDZxQVa +y849UWoj92TYQVONCiULL3uYjn/BgJto6hn1GbtzrvmYB56ZG03OA1UTPubb3wQ2 +o0ao+TY2/ekEn9cJrJsGWnrUIt7Pt1DaHmNQUQnoUlyN4odL4zBevnq3YhJ18s0x +i1UjGO9dBxjHlWZLiYipPGPmBUa1BWLQZd0fpK+l0pXR+MT0o6GOx0F8pDNl9xCW +6weDylOHC+OkzAJmeTEJLREpr3Lj7n7syQLff7z6d5lTNLtvo10XGHpP+kxemR/x +3zV/NWjhkEyWjB5bT+Xww51XF5fsz/tdILVqeTmLcgRlyi0uYlgzASjusyllUt68 +RDXXwSjxAgMBAAECggEAZ90ahaJMDH2ERsaeoo4e7uGjrKqo0jsrkEhm6tnHR7/l +gp1wWNaOaKDSG1aq7NqtAXL4Imroggv56TGrYWetf1+5OZTsCnkaz8Y8WBr/LIZZ +dp0a0dUdMhpXdTN/gh1zvCIbVcFTHoYYAjzxsGzcDHKIbeizzJIDeYVpoOlDQ9/9 +Bv6ft4mhaG5SHVnec9QdmbJnKDq5rI4aPXCCXOCzDjdTVfgntdH5TvoCH91ESSKw +kddciAbVsXoOWnBx3jKMj+hIA4F1p6nzZUbiVzmxhqfShQhDnCEvq8tF7KqRbUsS +Gx8MVtwSkEGaiJCDVjwSRGkghXlguNwZcfnWMtGMYQKBgQDmFWAApeXv4xXF2a/1 +HKumO5Z+w+XkKiM76YyTHTKO/KtDYRJiIlJMgx+hoRTBwlpYDrlbS9+Jnm7bZ9Ib +pxRyMAFRoV7eIhnoAn9KrxhS8xCYF2Km7U1lg/+m3pFKghjV4+K1GHbggmvoiIY1 +2t250zkZSslwTxu/2+jRKYOptQKBgQDlfYrzvuGqClJ9QClxlOV2UrWiGxq6eTgL +4V3l0HwPU9OW/hX0Hued8S70Dpb3o+AAyptbcAqFjSdyIPMbCfKLQQkKpfBUtOvb +Nm12z/VNKNZbu7kvaOJHunQNHzyMEHcjsB9daAVI0gJZKN+m6Qh4VF4jao7G9GNR +d7ge0KcXzQKBgQCqf8p9kHJ9OsVmsTMgK1fTvrJ+S8LvOn6TpjVCy08tAHYVXzjV +OePMyRpGluyfzNtQB9E5o1cKTzqNIjljvoN7PrGrgS6g45pZAIi9mlUnGvIAEsxL +MOy6vn9Tc/kswo2O6umUE4X8RwmZ7pmuDPtj+e+FG5N8w1Kn8VlsrhvgRQKBgAgz +clG/koTnFYeQUWrTrVeLIR6H5W6gglY6WYaq6qQJlNgigFpW+GP2iH0EQHTdEFY2 +51JfMKERKEW107o1ostDKbWNtIbyaDNPQJ4sVFHLkc15aea90shJa3hEk39V30wR +MS2/V+EAUEErasKmNT1Hlo2hczS86wewRY4kWrRJAoGAeYUG04cu55GwCgp50P3J +0NCNyiOkhnaj0wGPztMbDqNkaUAoaycoEsas5lhRAWT4YIVglz5pwR4uiI57w1cL +Mvjk5yDiQs7h3bV/qtm95YPBBC+y3mmZYlEA1lH0qktRNBlMVtfYkPztBh50UBOH +8qhIwqrpm3+JJ1p2p0XPl1c= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.pkcs1.key.pem new file mode 120000 index 00000000..6aba8b0c --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.pkcs1.key.pem @@ -0,0 +1 @@ +rsa2048.pkcs1.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.pub.pem new file mode 120000 index 00000000..518164eb --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS256.pub.pem @@ -0,0 +1 @@ +rsa2048.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem new file mode 100644 index 00000000..9f6e42e3 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.crt.pem @@ -0,0 +1,41 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN CERTIFICATE----- +MIIERDCCAqwCCQDqxucO41yAmTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDQ1OFoYDzMwMjAwMjExMjMwNDU4WjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIIBojANBgkqhkiG9w0BAQEF +AAOCAY8AMIIBigKCAYEA0DmQS4Xtgu5xtnQdxkuzB1j+W4OvNEOVOsg3Zcn9W1d5 +NowtngUh9K9vwBpl59M2j5PHj9dseEIuRqr++ZnZhBMlh/lLiIQ0oQ3cEa7wrwJO +i9ycZZbeNDHVNzAdQZEQR1DIMUhSTEMR96pVD4a9DzBKLQJaKxZRUOrMhf3QhAZ5 +9m0d/Kqu1Dm6YeWMLQQEewAaSQ9g22gty1EcLAvp/vnhR/DJSYIHCayd5mZwvk9q +WkXySfUmJGP70GFGG4GOnVLLkmCQgfT+OrzTtiIzNT26mtAsoUMnoD42TTBkR0aL +hULcj1MYUcB9uVCmJTvYuiCTODoNlL8T40r9L99HoHlTWVi9vf6I1vyNdHp3dXKB +MVL13+i5BeNIjADr0KKs0jtEEicWyuVhJz3rPLzBbPhz/DGQ/hTj/DRSdo9L9YA4 +WkqgD8uFUvIEKAJ/hXYx3QPEiIMU7hT4jk2Z2SIBiKKiNW4E/20EZOFmaeNWQaqq +mwX0cHtMygJtTYnEJ1IDAgMBAAEwDQYJKoZIhvcNAQELBQADggGBAGKkmv6d372z +Ujt1qjsjH7LHfIsPXdvnp7OhvujDEtY7dzwDCtR40zgB2qp+iXUO61FXErx8yDp9 +l7sDzk0AjY7RupANuo/3FyDuo0WoTUV3CJNnXf3Mrvu/DMjbaS6D4Jryz/HLE+2r +GYtdm165FZK/hQXuFfurkc4yqjrX90Wr+YHeen2y5Wk3jeUknmdp97F6+zkq6N5D +dKjy/ZOvy+1huNd5bzvJoiZLKqdSh/RQUoU6AP1p+83lo+7cPvS/zm/HvwxwMamA +1Cip1FypNxUxt5HR4bC5LwEvMTZ/+UTEelbyfjMdYU97aa58nPoMxf7DRBbr0tfj +GItI+mMoAw60eIaDbTncXvO1LVrFF5BfzVOTQ8ioPRwI7A5LMSC5JvxW8KsW2VX0 +vGwRbw8I6HXGRbBZ3zwmAK73q7go31+Dl/5VPFo+fVTL0P7/k/g0ZAtCu4/Wly9e +DLnYMoZbIF5lgp9cAzPOaWXiInsA6HSdgFUfXsBemRpholuw+Sacxg== +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem new file mode 100644 index 00000000..4fd9098b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.key.pem @@ -0,0 +1,56 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN PRIVATE KEY----- +MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDQOZBLhe2C7nG2 +dB3GS7MHWP5bg680Q5U6yDdlyf1bV3k2jC2eBSH0r2/AGmXn0zaPk8eP12x4Qi5G +qv75mdmEEyWH+UuIhDShDdwRrvCvAk6L3Jxllt40MdU3MB1BkRBHUMgxSFJMQxH3 +qlUPhr0PMEotAlorFlFQ6syF/dCEBnn2bR38qq7UObph5YwtBAR7ABpJD2DbaC3L +URwsC+n++eFH8MlJggcJrJ3mZnC+T2paRfJJ9SYkY/vQYUYbgY6dUsuSYJCB9P46 +vNO2IjM1Pbqa0CyhQyegPjZNMGRHRouFQtyPUxhRwH25UKYlO9i6IJM4Og2UvxPj +Sv0v30egeVNZWL29/ojW/I10end1coExUvXf6LkF40iMAOvQoqzSO0QSJxbK5WEn +Pes8vMFs+HP8MZD+FOP8NFJ2j0v1gDhaSqAPy4VS8gQoAn+FdjHdA8SIgxTuFPiO +TZnZIgGIoqI1bgT/bQRk4WZp41ZBqqqbBfRwe0zKAm1NicQnUgMCAwEAAQKCAYAg +ewo+LasKBIXqbxyB5ScNG126CsWWwoARxk+V6jdCO1fmIWGwR56vW3p0HeoNio31 +QZkcn/8El1Y+ocfaSZx7lL0DA+k7Z1wKT24nuAFFW3fDK2ueETWiMK/QxwmZQ7al +WT2RKnXj/YZc+s3/+QWey+qWMMq98+JFXAsBT8FqBtSZkxXdZwaUhljDkpoWH41P +Xom7IdH7B7o0//cEC+u5YWM55J6Rf933LV0IJqypkxvE7ypHTR1hCdOrArF78u5z +Jg61hZRDi9t+X2RNZZ027ysrVLU/gre6XzSZI1a7NygDOSWBmcycQBAf6ZYJDdeb +mLy5M62K0fNavaxiuspA/WD3k4BsXSsK/rGNU6DvpeuymEbWFzPIoD5uKWTwHdSa +5ZrJGcR+Q5D12EersJi3jm52tYqYE91sJ8x+q6Ko+u7kWSbUCssqJLITdCqBdoEL +tpZspCzfCShJ+7CqlC1jEAIRdYFWFgIk76eLyr1k8aYI+NBqwfQbTzNK9Okj07kC +gcEA6BSD2iW2KEHyPi10BsqiWLKWCS6e5UjVBZEgD7+c6pYABxvXrMCKgseyd4LY +FBJ15MLNp3KS1vozlQEYp7LFAhpNeYMADql39ZNc7FQPcv+QsyQfDLP26eypabhN +BDexMcBY4jhZNkEBXjdxU9l0rGCQw82qLO5mK4WuKfyj+IX0iQv6BOzfBTKYNsgt +JAb289KeyrsV7rAoxxxfmsjYqsQadeCaOMQfkATAKyVaMrs6aJfJokuz5ibv+PRz +p6JdAoHBAOWvnzNNUF5BAanmk6BeiYK1tf9xAjJ5tAOAdqqflbDVBlZVE8qGYOH2 +J7x2/LQVz+Dm3chC5AdUL0tu5qZ+rAr8Vc7lJDvGkbcfTaEv4/VbF1gyDwi+dwn1 +MV3WQMEuFrqLDa1G3zxYER6PsO1DcwMTMRiWWlF6F77FnRm1zmleIkeXEOPW4a3J +Id2W043od/tbNr0j4sU/Ha3M+Eb91XjkSVulsABL+98CP/BnqWEFQABgQmfBsXMD +Kla06hw/3wKBwHm7iQ28CjhDnxUOMnX9g/qScjCOy7no4hPxc6fPEjfaRll0OUTc +GctPhEU71Ktyo3RC2iyi5HLu+m+GC7CrDLt1oH3EQRtvuQSPL4am8ROZCgVtRPwc +yb8Z7CMQERXNQJygD/9ZHzJeFqGc40zgG1rvq/+IuWKoCd96V0iexENvwDzCk3pR +5QmM6FqT1Vm4bYCnUbN1PqPcswb90wgVodCw3FBIZ5yvAv8//qyjAxTpMFH8jD8d +BlgKxIUJdEDR4QKBwGZapfJBsN/fzjL9aqobluHlwg3sOVNvArZQyBDu/tEHjURp +s2EcEw5/GGQXDjPeSH3rw8ebb2yIqm7OJADsEBTxL/f8CvKMYaEeVQTQh6BuEHAg +Fq0J25hXaMFtWfv8YuqMTvL50z9b630YAXsqBJXJNqbDUcpfQzejbofnie1QoqwO +eNtfhcBhEjNiJDJn9xfPJQySclr97mbmIXnZYgj2im5J3q2zLrHJmd6zAzsWENha +DR2Zpk8fiP2Mr4sZNwKBwQDX2Y/Ycr/IQtNdP/YsFIDeNHJ+dBLDM4JJCetlbzsY +poIKM9+ZvC9EST0KoEumhUT4Fy75b75nbzRGRDmFDNyOxRcHixFVnbgZqWyAeCbw +xNCKrIbtrXk5JvFy5y0yjMdBeB2uB1KJZhesuwUS1JnhA8RDapQ7ZwpoleRd+iqg +3RJTtcvo5Ky6vdz74isxBL8WH+PMqQEm1el8Jwix5dHx5mKH5QM2XnkUm78V/NX9 +5I2wbxUhb3FO7gj9pxJbwX4= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.pkcs1.key.pem new file mode 120000 index 00000000..75de2b26 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.pkcs1.key.pem @@ -0,0 +1 @@ +rsa3072.pkcs1.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.pub.pem new file mode 120000 index 00000000..6d0de80e --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS384.pub.pem @@ -0,0 +1 @@ +rsa3072.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem new file mode 100644 index 00000000..55a08c37 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.crt.pem @@ -0,0 +1,47 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN CERTIFICATE----- +MIIFRDCCAywCCQC4g2isVGolKjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY +MBYGA1UECgwPanNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MCAXDTIwMDIw +MzIzMDY1MloYDzMwMjAwMjExMjMwNjUyWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEYMBYGA1UECgwP +anNvbndlYnRva2VuLmlvMQ0wCwYDVQQLDARqand0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAw7jTXeRwCRrDdYnrIwcLvSNfhpmJZ0ap1jzKVgUyCOYL +TaB9+naJRjHqx7B5wgx/ArRF2nluQ5tawZPMFw2I/iqXYrQEPTbq1XVugYC/C491 +bcXOTKx+DgEvnhNysm/KmzFsEcw78prB5sIAZSR+S5zZPuny4zww2UzE9TZ433RB +kyA+wVkd64bgXdkMrVc+gsRsOtvwPFbQ89zg8d/pNV0mDtjDsfiYw0pSAah11fJG +a+aRc46CqFu/6rHuN4uq6542LdtshPbHz29VKHxk8agtcx06+8F05Bg4LFm1rRhY +0g3KsT7s8XHMVdo9h2bIQuWOaFg3mehpH6ZYBV4ENo98V/jDaUPpBHsaUXw4fG/w +rnI+YwRjGlmp2QEr5VRfh0x8Addf6N64lmbQUpCPhJweJd54D3JvIpJr8HiG3GYW +eFsrmDzmhrozZHxE4P7UesW6lWwQzGfwYGXs7j6TEa2hZ8EB/t1jsYNjZ5UYY/Jb +KgFGSkMGje4Bi5Bv6kh4+pp3DT5QsG/AfLVlr5ineLDWkJ15uZjOxl12EOPXOCWV +iVqS6rayJfb95YJQ52rT4H83BsApHbzFj7q/CIaeJkUdv9GJ2SOADcXdgG7Xk7tH +qb1VIH4zRBo5mc0qN1cAwjopxHv2h3tGaTHKbptvfiLulH+AvvuWmLMEQo6ZDlsC +AwEAATANBgkqhkiG9w0BAQsFAAOCAgEApnHjMQwt5hm6UlEDvdWCYbh7ctkLbgwR +iBP1lvunm2oF0jGpipt8oDR/TT43usb6ieuU+ABksjxOROeoVZbK8bEpnzeo3nNE +41ERI3Byjp7tsja8QGG0uBk9QZ0+7MhJqhEDVAIbS0Lf4exkWiLZrW7ogAEFYKTN +DE6CxOcfR/kXj6ejuCnvN4xYqnw8G/OF/3tnHMfKnnnqtMmWdAKd3Y5S1EJZ5vtp +lZ3I9HA5Hx0sTH1ruCOIRzaC5En1c6zW1HjxmeAqLeG814gezlEhHzb4SCkabgQh +Bq15O8eQaW92f8xZoUQN25w7SNYszk9AdhroJz3+BOzG3+Y1EInLk5hDHT8oUNFz +e8EosJEwJDK3wq9YOhn8PUT/DacyNKONJVNly3fTBXoSR3oReW61p6T19z4AYsY9 +qMwSjIL2UcgAF8Kpsx2NdQrDfdveNMhul7AjIgz+e2DtRqCkZ6ypdhht2pmlpiXO +TiUG/1OBq2yTeJF9LjAUzsSNnsZ/F8pJbwSpr7VqDmTNGTfrh6x1ojHNFjJeTqK8 +MCTmQtJJTAbV4nuB+thFFWDx0IWvbG7ViYds9sdJNO4L3baXeAioJhHs5buBy3eb +ZWjLAwHpSCqNY3d6+ouGLwE1YVFsk8sV9UM+gl15VynKkunbYoKhiD82HGASNYtE +33eif1l5Nk0= +-----END CERTIFICATE----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem new file mode 100644 index 00000000..cb8296b2 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.key.pem @@ -0,0 +1,68 @@ +# +# Copyright © 2020 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. +# + +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDDuNNd5HAJGsN1 +iesjBwu9I1+GmYlnRqnWPMpWBTII5gtNoH36dolGMerHsHnCDH8CtEXaeW5Dm1rB +k8wXDYj+KpditAQ9NurVdW6BgL8Lj3Vtxc5MrH4OAS+eE3Kyb8qbMWwRzDvymsHm +wgBlJH5LnNk+6fLjPDDZTMT1NnjfdEGTID7BWR3rhuBd2QytVz6CxGw62/A8VtDz +3ODx3+k1XSYO2MOx+JjDSlIBqHXV8kZr5pFzjoKoW7/qse43i6rrnjYt22yE9sfP +b1UofGTxqC1zHTr7wXTkGDgsWbWtGFjSDcqxPuzxccxV2j2HZshC5Y5oWDeZ6Gkf +plgFXgQ2j3xX+MNpQ+kEexpRfDh8b/Cucj5jBGMaWanZASvlVF+HTHwB11/o3riW +ZtBSkI+EnB4l3ngPcm8ikmvweIbcZhZ4WyuYPOaGujNkfETg/tR6xbqVbBDMZ/Bg +ZezuPpMRraFnwQH+3WOxg2NnlRhj8lsqAUZKQwaN7gGLkG/qSHj6mncNPlCwb8B8 +tWWvmKd4sNaQnXm5mM7GXXYQ49c4JZWJWpLqtrIl9v3lglDnatPgfzcGwCkdvMWP +ur8Ihp4mRR2/0YnZI4ANxd2AbteTu0epvVUgfjNEGjmZzSo3VwDCOinEe/aHe0Zp +Mcpum29+Iu6Uf4C++5aYswRCjpkOWwIDAQABAoICACPSgUUvGV5hOqMZsiLAGGLu +xX4iPebcJRukFrh1zPmZ+TmlBUnBRlDFtB4Ga9KbbOe2zQ42qXrQRWUmwvT5Mjiq +3Phg0GHP2l1lV+t2AAGCqVCFIsQf0haIGwoIrzZ/hYqwGgKL6fD2aETu/xmD+2Wl +eJGuShlTG/G5vlbPOIJVieb+wN2sjPBdyFUE8/AKBtPyVYjUVn0EusvXgohinhF5 +UgznmbHKOVONF8Nb7O1SoZcAJWEMFVfxKwguttYNxyPG2k28WnlfnaSW0PRPCD6+ +tErcb75CYz2YPTfI15qt2RvhEFcumDl8xZR1FEvjAQZVc6Ife1W9FviG/pdE5Oox +lzsdOtIVSsrkDgl+kPmQczTdl8qWndh1c5rnOWALX388I+CWEgNDY0cfD6W2Xg6i +IIYCZ3mm0ZBZVTh1qCTciPFs6eBLZ2r/N+/dT0tTYrtKPKE8FfUqes9eFI9yEMmp +XKRw7tZZ78olS8eii1xiPsTSwNOoCFclyRzIE/Wfml2oAWkRiuC9tQZwkw6mj55p +5g1kxz0OtG+KrVaFxronaB1LLuNKJ41vRvmxevD6LnvGm/PMMkbizXGm7VPpaT9G +ETfNnk0ZKGSVemEmr+zrV2cAlAX7ZR+9ULY8DwjBaKO2g7w0ONqBdzuAXbP2T9WA +Zhmc3YiIgx1IdvH286YxAoIBAQDj2kJLqD8MOhTTNLwYQAc2WA8C1IxJWObShPNx +N2n7RQl7wL7gdphNQ4jbkbGEKqu4eJUlBBHPNUpTcaaYXRD0mNcGRXloXVSaVhLU +vXt6/hEiSk9TX6gGT8XGmQ0xfDZfT+yfrO+z6cABfMh1da8xKDYxg8lAok79jJRr +OWwzKsMj94LUOP7Z8feh4rjvrR0nHoSC4Ds8mrXOZTt42xMVunPxgHEf39Hx9Ikx +qiKvOZHdqRdru11xcD5AW6nvwgYKcTfvcKDFYXwfi0WMFvecSY9nK8Qg5ZMba0wI +pOlccoyat1EOy8aDCYr1OSmzhoQrCJGVTqyHDnce53FZQyQDAoIBAQDb5nM7Df11 +4JMDM0zbU90nDf+Qm6BXiHtLpADiTwf6NFLs26+u026Lo/60LxjHKif1UJPidma4 +b37Yeuwvlg2BHg+A1guYSv75k/bGIViGP3zDAWQRJ2/LonxBNcEGQToP7bBd5UM4 +dKCeWgU9PMF7qa1/xs5rrEAsqGNYrKu45Ng0YOm61NKL4zf5rUfEx/gX7v9NW/er +q9p0Ms5k1UK/AXaeMGhzT75vhoiMObMv2BKM/IAR0Wrm/xYCvnuey7hva+NRGOJ/ +gm9KnUxteafEqllA/VHgYpqZFEXwoR4Ty4ByBXDYTcLG5m7LynAfo5NUIHI3HB+v +uKV7hX3egJjJAoIBAQC3PVKth4vUqG0RAcr28Z8bPCwuSYLchctzqAojlb38niOn +S3X2DEolcNeCRSPut2ZMP2UqVKCB9Ehm3PJufAHjw3rBh2PA47XjPK9+OTgxzFs5 +KWusEDSPht31/iYXEt6jPiJ8s1Y+aRDJ4XFQzSjsLnuOzH4wJZfC3qiJpq92YsB2 +j1m+lGuYGLjejvfNgHn+eNN2cSASeBUX/F+crQonIkCWCoZvbM9pdxBSSZIFOxYs +ngzAzfiy/uKBXXZH49B5211xiTEyK1joAVgX9myBWsMh5JehIR9yIJMQLJejile7 +IQvmC0kFHsqKtcLsppRqC0URPykOoDp6NwT4FT/DAoIBAQDXmhtgy1a3PHjnqmSw +pokuwYrRPcT4DdjVUPeM6+/mYWbs1Hhr8OFyCFiyUXr5y1tiKp7Ua0JLkwXLOrpX +7cdP0SliKHs11lIoYeqSWB9zgMvSZoq2RvRVs/of9ZRLjahf9av2Y9KEh9TzbU+1 +utv5Y2O45DN/XmONZYwCZUn4/mb89Ag2JnRIs38uTbcQOQAGd03Zi1JJ/zUwuJ+k +PXQz0jt63fuLE6SjtEQtOGV3g2Ks2OS4k5s84N2z0w9holwy4pT97mgknL6BabiF +ncHgESVxku20EvmBHV91joLu5ZgKM0twyM0wNr5rERDd9IN++FEDt49ZurCFa1z9 +yxgBAoIBAC3HJzGb7Cufqw1JNng8H1mkJ5+1ZCNo7jy/aUYd5OacGTCNTcvuPTj+ +2iGvn4G0JR7pukhU5dVtGMQGpmmp8zk6/xzmyqeeiQNi4wdEgMALq4I0nynXkxDv +utKsXpmPiwyxmwCg9EY7AokfGWbxI5Yf7HkrjxME7jHz31lt5OF7AKyE1veFYWRa +puP1KVjNH7UAoE3WHnPnj7xvfQspXVRpzPWXH86XVonqnjQgu3SDkclPbkjg4HVj +athb6h5RN5bYx1cbUvo3JssBYl92FlXPU9lLzgv4nALUdVSi8PjbjQ7WXdxaKPdf +lczRTJNTE/KNUE0pkC5P4c/e0A1OFu0= +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.pkcs1.key.pem new file mode 120000 index 00000000..15d6f5c0 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.pkcs1.key.pem @@ -0,0 +1 @@ +rsa4096.pkcs1.key.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.pub.pem new file mode 120000 index 00000000..74c57d7f --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/RS512.pub.pem @@ -0,0 +1 @@ +rsa4096.pub.pem \ No newline at end of file diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/X25519.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/X25519.key.pem new file mode 100644 index 00000000..2e5ceb02 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/X25519.key.pem @@ -0,0 +1,19 @@ +# +# 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. +# + +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIOh9nwGOIRA3+m2SHuDfyk8b/IiS5pkI5wWBFCchOXB+ +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/X25519.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/X25519.pub.pem new file mode 100644 index 00000000..502f13a7 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/X25519.pub.pem @@ -0,0 +1,19 @@ +# +# 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. +# + +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VuAyEAAJ9DwNIJMibJk/qlttl4EdoJZdSNtd7ECXPikXTAEE0= +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/X448.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/X448.key.pem new file mode 100644 index 00000000..f11038b0 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/X448.key.pem @@ -0,0 +1,20 @@ +# +# 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. +# + +-----BEGIN PRIVATE KEY----- +MEYCAQAwBQYDK2VvBDoEONhB6qRVbPgGBUAZLC6yhwMQAMTKfcpebGZJj55Btlt9 +lpOIfqEM3wKw2RGuqtjPt+E8Nc+glFzV +-----END PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/X448.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/X448.pub.pem new file mode 100644 index 00000000..3b7f5a59 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/X448.pub.pem @@ -0,0 +1,20 @@ +# +# 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. +# + +-----BEGIN PUBLIC KEY----- +MEIwBQYDK2VvAzkA/XW37ksNpY3J7qglqWh56nZP3WgdrJlMtxPaplYn4zkPBZKa +nWlk2gR+m1xO2NXAOL3JZhHQBCc= +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.crt.pem similarity index 67% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.crt.pem index 11cbfe76..45195339 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.crt.pem +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.crt.pem @@ -1,3 +1,19 @@ +# +# Copyright © 2020 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. +# + -----BEGIN CERTIFICATE----- MIIDRDCCAiwCCQCgd9OzR40NCDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.key.pem similarity index 74% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.key.pem index 418581f2..61088c2f 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa2048.key.pem +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.key.pem @@ -1,3 +1,19 @@ +# +# Copyright © 2020 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. +# + -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOQfQzDFDZxQVa y849UWoj92TYQVONCiULL3uYjn/BgJto6hn1GbtzrvmYB56ZG03OA1UTPubb3wQ2 diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.pkcs1.key.pem new file mode 100644 index 00000000..9e995d1b --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.pkcs1.key.pem @@ -0,0 +1,43 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAzkH0MwxQ2cUFWsvOPVFqI/dk2EFTjQolCy97mI5/wYCbaOoZ +9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2Nv3pBJ/XCaybBlp61CLez7dQ2h5j +UFEJ6FJcjeKHS+MwXr56t2ISdfLNMYtVIxjvXQcYx5VmS4mIqTxj5gVGtQVi0GXd +H6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pThwvjpMwCZnkxCS0RKa9y4+5+7MkC +33+8+neZUzS7b6NdFxh6T/pMXpkf8d81fzVo4ZBMloweW0/l8MOdVxeX7M/7XSC1 +ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo8QIDAQABAoIBAGfdGoWiTAx9hEbG +nqKOHu7ho6yqqNI7K5BIZurZx0e/5YKdcFjWjmig0htWquzarQFy+CJq6IIL+ekx +q2FnrX9fuTmU7Ap5Gs/GPFga/yyGWXadGtHVHTIaV3Uzf4Idc7wiG1XBUx6GGAI8 +8bBs3AxyiG3os8ySA3mFaaDpQ0Pf/Qb+n7eJoWhuUh1Z3nPUHZmyZyg6uayOGj1w +glzgsw43U1X4J7XR+U76Ah/dREkisJHXXIgG1bF6Dlpwcd4yjI/oSAOBdaep82VG +4lc5sYan0oUIQ5whL6vLReyqkW1LEhsfDFbcEpBBmoiQg1Y8EkRpIIV5YLjcGXH5 +1jLRjGECgYEA5hVgAKXl7+MVxdmv9RyrpjuWfsPl5CojO+mMkx0yjvyrQ2ESYiJS +TIMfoaEUwcJaWA65W0vfiZ5u22fSG6cUcjABUaFe3iIZ6AJ/Sq8YUvMQmBdipu1N +ZYP/pt6RSoIY1ePitRh24IJr6IiGNdrdudM5GUrJcE8bv9vo0SmDqbUCgYEA5X2K +877hqgpSfUApcZTldlK1ohsaunk4C+Fd5dB8D1PTlv4V9B7nnfEu9A6W96PgAMqb +W3AKhY0nciDzGwnyi0EJCqXwVLTr2zZtds/1TSjWW7u5L2jiR7p0DR88jBB3I7Af +XWgFSNICWSjfpukIeFReI2qOxvRjUXe4HtCnF80CgYEAqn/KfZByfTrFZrEzICtX +076yfkvC7zp+k6Y1QstPLQB2FV841TnjzMkaRpbsn8zbUAfROaNXCk86jSI5Y76D +ez6xq4EuoOOaWQCIvZpVJxryABLMSzDsur5/U3P5LMKNjurplBOF/EcJme6Zrgz7 +Y/nvhRuTfMNSp/FZbK4b4EUCgYAIM3JRv5KE5xWHkFFq061XiyEeh+VuoIJWOlmG +quqkCZTYIoBaVvhj9oh9BEB03RBWNudSXzChEShFtdO6NaLLQym1jbSG8mgzT0Ce +LFRRy5HNeWnmvdLISWt4RJN/Vd9METEtv1fhAFBBK2rCpjU9R5aNoXM0vOsHsEWO +JFq0SQKBgHmFBtOHLueRsAoKedD9ydDQjcojpIZ2o9MBj87TGw6jZGlAKGsnKBLG +rOZYUQFk+GCFYJc+acEeLoiOe8NXCzL45Ocg4kLO4d21f6rZveWDwQQvst5pmWJR +ANZR9KpLUTQZTFbX2JD87QYedFATh/KoSMKq6Zt/iSdadqdFz5dX +-----END RSA PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.pub.pem new file mode 100644 index 00000000..88c76f3e --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa2048.pub.pem @@ -0,0 +1,25 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzkH0MwxQ2cUFWsvOPVFq +I/dk2EFTjQolCy97mI5/wYCbaOoZ9Rm7c675mAeemRtNzgNVEz7m298ENqNGqPk2 +Nv3pBJ/XCaybBlp61CLez7dQ2h5jUFEJ6FJcjeKHS+MwXr56t2ISdfLNMYtVIxjv +XQcYx5VmS4mIqTxj5gVGtQVi0GXdH6SvpdKV0fjE9KOhjsdBfKQzZfcQlusHg8pT +hwvjpMwCZnkxCS0RKa9y4+5+7MkC33+8+neZUzS7b6NdFxh6T/pMXpkf8d81fzVo +4ZBMloweW0/l8MOdVxeX7M/7XSC1ank5i3IEZcotLmJYMwEo7rMpZVLevEQ118Eo +8QIDAQAB +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.crt.pem similarity index 72% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.crt.pem index a93fa833..9f6e42e3 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.crt.pem +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.crt.pem @@ -1,3 +1,19 @@ +# +# Copyright © 2020 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. +# + -----BEGIN CERTIFICATE----- MIIERDCCAqwCCQDqxucO41yAmTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.key.pem similarity index 80% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.key.pem index 1b9b8708..4fd9098b 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa3072.key.pem +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.key.pem @@ -1,3 +1,19 @@ +# +# Copyright © 2020 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. +# + -----BEGIN PRIVATE KEY----- MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDQOZBLhe2C7nG2 dB3GS7MHWP5bg680Q5U6yDdlyf1bV3k2jC2eBSH0r2/AGmXn0zaPk8eP12x4Qi5G diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.pkcs1.key.pem new file mode 100644 index 00000000..4be97fc3 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.pkcs1.key.pem @@ -0,0 +1,55 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN RSA PRIVATE KEY----- +MIIG4wIBAAKCAYEA0DmQS4Xtgu5xtnQdxkuzB1j+W4OvNEOVOsg3Zcn9W1d5Nowt +ngUh9K9vwBpl59M2j5PHj9dseEIuRqr++ZnZhBMlh/lLiIQ0oQ3cEa7wrwJOi9yc +ZZbeNDHVNzAdQZEQR1DIMUhSTEMR96pVD4a9DzBKLQJaKxZRUOrMhf3QhAZ59m0d +/Kqu1Dm6YeWMLQQEewAaSQ9g22gty1EcLAvp/vnhR/DJSYIHCayd5mZwvk9qWkXy +SfUmJGP70GFGG4GOnVLLkmCQgfT+OrzTtiIzNT26mtAsoUMnoD42TTBkR0aLhULc +j1MYUcB9uVCmJTvYuiCTODoNlL8T40r9L99HoHlTWVi9vf6I1vyNdHp3dXKBMVL1 +3+i5BeNIjADr0KKs0jtEEicWyuVhJz3rPLzBbPhz/DGQ/hTj/DRSdo9L9YA4Wkqg +D8uFUvIEKAJ/hXYx3QPEiIMU7hT4jk2Z2SIBiKKiNW4E/20EZOFmaeNWQaqqmwX0 +cHtMygJtTYnEJ1IDAgMBAAECggGAIHsKPi2rCgSF6m8cgeUnDRtdugrFlsKAEcZP +leo3QjtX5iFhsEeer1t6dB3qDYqN9UGZHJ//BJdWPqHH2kmce5S9AwPpO2dcCk9u +J7gBRVt3wytrnhE1ojCv0McJmUO2pVk9kSp14/2GXPrN//kFnsvqljDKvfPiRVwL +AU/BagbUmZMV3WcGlIZYw5KaFh+NT16JuyHR+we6NP/3BAvruWFjOeSekX/d9y1d +CCasqZMbxO8qR00dYQnTqwKxe/LucyYOtYWUQ4vbfl9kTWWdNu8rK1S1P4K3ul80 +mSNWuzcoAzklgZnMnEAQH+mWCQ3Xm5i8uTOtitHzWr2sYrrKQP1g95OAbF0rCv6x +jVOg76XrsphG1hczyKA+bilk8B3UmuWayRnEfkOQ9dhHq7CYt45udrWKmBPdbCfM +fquiqPru5Fkm1ArLKiSyE3QqgXaBC7aWbKQs3wkoSfuwqpQtYxACEXWBVhYCJO+n +i8q9ZPGmCPjQasH0G08zSvTpI9O5AoHBAOgUg9oltihB8j4tdAbKoliylgkunuVI +1QWRIA+/nOqWAAcb16zAioLHsneC2BQSdeTCzadyktb6M5UBGKeyxQIaTXmDAA6p +d/WTXOxUD3L/kLMkHwyz9unsqWm4TQQ3sTHAWOI4WTZBAV43cVPZdKxgkMPNqizu +ZiuFrin8o/iF9IkL+gTs3wUymDbILSQG9vPSnsq7Fe6wKMccX5rI2KrEGnXgmjjE +H5AEwCslWjK7OmiXyaJLs+Ym7/j0c6eiXQKBwQDlr58zTVBeQQGp5pOgXomCtbX/ +cQIyebQDgHaqn5Ww1QZWVRPKhmDh9ie8dvy0Fc/g5t3IQuQHVC9LbuamfqwK/FXO +5SQ7xpG3H02hL+P1WxdYMg8IvncJ9TFd1kDBLha6iw2tRt88WBEej7DtQ3MDEzEY +llpRehe+xZ0Ztc5pXiJHlxDj1uGtySHdltON6Hf7Wza9I+LFPx2tzPhG/dV45Elb +pbAAS/vfAj/wZ6lhBUAAYEJnwbFzAypWtOocP98CgcB5u4kNvAo4Q58VDjJ1/YP6 +knIwjsu56OIT8XOnzxI32kZZdDlE3BnLT4RFO9SrcqN0QtosouRy7vpvhguwqwy7 +daB9xEEbb7kEjy+GpvETmQoFbUT8HMm/GewjEBEVzUCcoA//WR8yXhahnONM4Bta +76v/iLliqAnfeldInsRDb8A8wpN6UeUJjOhak9VZuG2Ap1GzdT6j3LMG/dMIFaHQ +sNxQSGecrwL/P/6sowMU6TBR/Iw/HQZYCsSFCXRA0eECgcBmWqXyQbDf384y/Wqq +G5bh5cIN7DlTbwK2UMgQ7v7RB41EabNhHBMOfxhkFw4z3kh968PHm29siKpuziQA +7BAU8S/3/AryjGGhHlUE0IegbhBwIBatCduYV2jBbVn7/GLqjE7y+dM/W+t9GAF7 +KgSVyTamw1HKX0M3o26H54ntUKKsDnjbX4XAYRIzYiQyZ/cXzyUMknJa/e5m5iF5 +2WII9opuSd6tsy6xyZneswM7FhDYWg0dmaZPH4j9jK+LGTcCgcEA19mP2HK/yELT +XT/2LBSA3jRyfnQSwzOCSQnrZW87GKaCCjPfmbwvREk9CqBLpoVE+Bcu+W++Z280 +RkQ5hQzcjsUXB4sRVZ24GalsgHgm8MTQiqyG7a15OSbxcuctMozHQXgdrgdSiWYX +rLsFEtSZ4QPEQ2qUO2cKaJXkXfoqoN0SU7XL6OSsur3c++IrMQS/Fh/jzKkBJtXp +fCcIseXR8eZih+UDNl55FJu/FfzV/eSNsG8VIW9xTu4I/acSW8F+ +-----END RSA PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.pub.pem new file mode 100644 index 00000000..3ab8814a --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa3072.pub.pem @@ -0,0 +1,27 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0DmQS4Xtgu5xtnQdxkuz +B1j+W4OvNEOVOsg3Zcn9W1d5NowtngUh9K9vwBpl59M2j5PHj9dseEIuRqr++ZnZ +hBMlh/lLiIQ0oQ3cEa7wrwJOi9ycZZbeNDHVNzAdQZEQR1DIMUhSTEMR96pVD4a9 +DzBKLQJaKxZRUOrMhf3QhAZ59m0d/Kqu1Dm6YeWMLQQEewAaSQ9g22gty1EcLAvp +/vnhR/DJSYIHCayd5mZwvk9qWkXySfUmJGP70GFGG4GOnVLLkmCQgfT+OrzTtiIz +NT26mtAsoUMnoD42TTBkR0aLhULcj1MYUcB9uVCmJTvYuiCTODoNlL8T40r9L99H +oHlTWVi9vf6I1vyNdHp3dXKBMVL13+i5BeNIjADr0KKs0jtEEicWyuVhJz3rPLzB +bPhz/DGQ/hTj/DRSdo9L9YA4WkqgD8uFUvIEKAJ/hXYx3QPEiIMU7hT4jk2Z2SIB +iKKiNW4E/20EZOFmaeNWQaqqmwX0cHtMygJtTYnEJ1IDAgMBAAE= +-----END PUBLIC KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.crt.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.crt.pem similarity index 76% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.crt.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.crt.pem index c63b1941..55a08c37 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.crt.pem +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.crt.pem @@ -1,3 +1,19 @@ +# +# Copyright © 2020 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. +# + -----BEGIN CERTIFICATE----- MIIFRDCCAywCCQC4g2isVGolKjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEY diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.key.pem similarity index 84% rename from impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.key.pem rename to impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.key.pem index 6c388705..cb8296b2 100644 --- a/impl/src/test/resources/io/jsonwebtoken/impl/crypto/rsa4096.key.pem +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.key.pem @@ -1,3 +1,19 @@ +# +# Copyright © 2020 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. +# + -----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDDuNNd5HAJGsN1 iesjBwu9I1+GmYlnRqnWPMpWBTII5gtNoH36dolGMerHsHnCDH8CtEXaeW5Dm1rB diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.pkcs1.key.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.pkcs1.key.pem new file mode 100644 index 00000000..275d9a57 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.pkcs1.key.pem @@ -0,0 +1,67 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAw7jTXeRwCRrDdYnrIwcLvSNfhpmJZ0ap1jzKVgUyCOYLTaB9 ++naJRjHqx7B5wgx/ArRF2nluQ5tawZPMFw2I/iqXYrQEPTbq1XVugYC/C491bcXO +TKx+DgEvnhNysm/KmzFsEcw78prB5sIAZSR+S5zZPuny4zww2UzE9TZ433RBkyA+ +wVkd64bgXdkMrVc+gsRsOtvwPFbQ89zg8d/pNV0mDtjDsfiYw0pSAah11fJGa+aR +c46CqFu/6rHuN4uq6542LdtshPbHz29VKHxk8agtcx06+8F05Bg4LFm1rRhY0g3K +sT7s8XHMVdo9h2bIQuWOaFg3mehpH6ZYBV4ENo98V/jDaUPpBHsaUXw4fG/wrnI+ +YwRjGlmp2QEr5VRfh0x8Addf6N64lmbQUpCPhJweJd54D3JvIpJr8HiG3GYWeFsr +mDzmhrozZHxE4P7UesW6lWwQzGfwYGXs7j6TEa2hZ8EB/t1jsYNjZ5UYY/JbKgFG +SkMGje4Bi5Bv6kh4+pp3DT5QsG/AfLVlr5ineLDWkJ15uZjOxl12EOPXOCWViVqS +6rayJfb95YJQ52rT4H83BsApHbzFj7q/CIaeJkUdv9GJ2SOADcXdgG7Xk7tHqb1V +IH4zRBo5mc0qN1cAwjopxHv2h3tGaTHKbptvfiLulH+AvvuWmLMEQo6ZDlsCAwEA +AQKCAgAj0oFFLxleYTqjGbIiwBhi7sV+Ij3m3CUbpBa4dcz5mfk5pQVJwUZQxbQe +BmvSm2znts0ONql60EVlJsL0+TI4qtz4YNBhz9pdZVfrdgABgqlQhSLEH9IWiBsK +CK82f4WKsBoCi+nw9mhE7v8Zg/tlpXiRrkoZUxvxub5WzziCVYnm/sDdrIzwXchV +BPPwCgbT8lWI1FZ9BLrL14KIYp4ReVIM55mxyjlTjRfDW+ztUqGXACVhDBVX8SsI +LrbWDccjxtpNvFp5X52kltD0Twg+vrRK3G++QmM9mD03yNeardkb4RBXLpg5fMWU +dRRL4wEGVXOiH3tVvRb4hv6XROTqMZc7HTrSFUrK5A4JfpD5kHM03ZfKlp3YdXOa +5zlgC19/PCPglhIDQ2NHHw+ltl4OoiCGAmd5ptGQWVU4dagk3IjxbOngS2dq/zfv +3U9LU2K7SjyhPBX1KnrPXhSPchDJqVykcO7WWe/KJUvHootcYj7E0sDTqAhXJckc +yBP1n5pdqAFpEYrgvbUGcJMOpo+eaeYNZMc9DrRviq1Whca6J2gdSy7jSieNb0b5 +sXrw+i57xpvzzDJG4s1xpu1T6Wk/RhE3zZ5NGShklXphJq/s61dnAJQF+2UfvVC2 +PA8IwWijtoO8NDjagXc7gF2z9k/VgGYZnN2IiIMdSHbx9vOmMQKCAQEA49pCS6g/ +DDoU0zS8GEAHNlgPAtSMSVjm0oTzcTdp+0UJe8C+4HaYTUOI25GxhCqruHiVJQQR +zzVKU3GmmF0Q9JjXBkV5aF1UmlYS1L17ev4RIkpPU1+oBk/FxpkNMXw2X0/sn6zv +s+nAAXzIdXWvMSg2MYPJQKJO/YyUazlsMyrDI/eC1Dj+2fH3oeK4760dJx6EguA7 +PJq1zmU7eNsTFbpz8YBxH9/R8fSJMaoirzmR3akXa7tdcXA+QFup78IGCnE373Cg +xWF8H4tFjBb3nEmPZyvEIOWTG2tMCKTpXHKMmrdRDsvGgwmK9Tkps4aEKwiRlU6s +hw53HudxWUMkAwKCAQEA2+ZzOw39deCTAzNM21PdJw3/kJugV4h7S6QA4k8H+jRS +7NuvrtNui6P+tC8Yxyon9VCT4nZmuG9+2HrsL5YNgR4PgNYLmEr++ZP2xiFYhj98 +wwFkESdvy6J8QTXBBkE6D+2wXeVDOHSgnloFPTzBe6mtf8bOa6xALKhjWKyruOTY +NGDputTSi+M3+a1HxMf4F+7/TVv3q6vadDLOZNVCvwF2njBoc0++b4aIjDmzL9gS +jPyAEdFq5v8WAr57nsu4b2vjURjif4JvSp1MbXmnxKpZQP1R4GKamRRF8KEeE8uA +cgVw2E3CxuZuy8pwH6OTVCByNxwfr7ile4V93oCYyQKCAQEAtz1SrYeL1KhtEQHK +9vGfGzwsLkmC3IXLc6gKI5W9/J4jp0t19gxKJXDXgkUj7rdmTD9lKlSggfRIZtzy +bnwB48N6wYdjwOO14zyvfjk4McxbOSlrrBA0j4bd9f4mFxLeoz4ifLNWPmkQyeFx +UM0o7C57jsx+MCWXwt6oiaavdmLAdo9ZvpRrmBi43o73zYB5/njTdnEgEngVF/xf +nK0KJyJAlgqGb2zPaXcQUkmSBTsWLJ4MwM34sv7igV12R+PQedtdcYkxMitY6AFY +F/ZsgVrDIeSXoSEfciCTECyXo4pXuyEL5gtJBR7KirXC7KaUagtFET8pDqA6ejcE ++BU/wwKCAQEA15obYMtWtzx456pksKaJLsGK0T3E+A3Y1VD3jOvv5mFm7NR4a/Dh +cghYslF6+ctbYiqe1GtCS5MFyzq6V+3HT9EpYih7NdZSKGHqklgfc4DL0maKtkb0 +VbP6H/WUS42oX/Wr9mPShIfU821Ptbrb+WNjuOQzf15jjWWMAmVJ+P5m/PQINiZ0 +SLN/Lk23EDkABndN2YtSSf81MLifpD10M9I7et37ixOko7RELThld4NirNjkuJOb +PODds9MPYaJcMuKU/e5oJJy+gWm4hZ3B4BElcZLttBL5gR1fdY6C7uWYCjNLcMjN +MDa+axEQ3fSDfvhRA7ePWbqwhWtc/csYAQKCAQAtxycxm+wrn6sNSTZ4PB9ZpCef +tWQjaO48v2lGHeTmnBkwjU3L7j04/tohr5+BtCUe6bpIVOXVbRjEBqZpqfM5Ov8c +5sqnnokDYuMHRIDAC6uCNJ8p15MQ77rSrF6Zj4sMsZsAoPRGOwKJHxlm8SOWH+x5 +K48TBO4x899ZbeThewCshNb3hWFkWqbj9SlYzR+1AKBN1h5z54+8b30LKV1Uacz1 +lx/Ol1aJ6p40ILt0g5HJT25I4OB1Y2rYW+oeUTeW2MdXG1L6NybLAWJfdhZVz1PZ +S84L+JwC1HVUovD4240O1l3cWij3X5XM0UyTUxPyjVBNKZAuT+HP3tANThbt +-----END RSA PRIVATE KEY----- diff --git a/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.pub.pem b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.pub.pem new file mode 100644 index 00000000..b86424f6 --- /dev/null +++ b/impl/src/test/resources/io/jsonwebtoken/impl/security/rsa4096.pub.pem @@ -0,0 +1,30 @@ +# +# Copyright © 2022 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. +# + +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw7jTXeRwCRrDdYnrIwcL +vSNfhpmJZ0ap1jzKVgUyCOYLTaB9+naJRjHqx7B5wgx/ArRF2nluQ5tawZPMFw2I +/iqXYrQEPTbq1XVugYC/C491bcXOTKx+DgEvnhNysm/KmzFsEcw78prB5sIAZSR+ +S5zZPuny4zww2UzE9TZ433RBkyA+wVkd64bgXdkMrVc+gsRsOtvwPFbQ89zg8d/p +NV0mDtjDsfiYw0pSAah11fJGa+aRc46CqFu/6rHuN4uq6542LdtshPbHz29VKHxk +8agtcx06+8F05Bg4LFm1rRhY0g3KsT7s8XHMVdo9h2bIQuWOaFg3mehpH6ZYBV4E +No98V/jDaUPpBHsaUXw4fG/wrnI+YwRjGlmp2QEr5VRfh0x8Addf6N64lmbQUpCP +hJweJd54D3JvIpJr8HiG3GYWeFsrmDzmhrozZHxE4P7UesW6lWwQzGfwYGXs7j6T +Ea2hZ8EB/t1jsYNjZ5UYY/JbKgFGSkMGje4Bi5Bv6kh4+pp3DT5QsG/AfLVlr5in +eLDWkJ15uZjOxl12EOPXOCWViVqS6rayJfb95YJQ52rT4H83BsApHbzFj7q/CIae +JkUdv9GJ2SOADcXdgG7Xk7tHqb1VIH4zRBo5mc0qN1cAwjopxHv2h3tGaTHKbptv +fiLulH+AvvuWmLMEQo6ZDlsCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/pom.xml b/pom.xml index b3a93037..d54ae3c5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ - + 4.0.0 io.jsonwebtoken @@ -85,35 +86,15 @@ true - - - - false - - bintray-jwtk-coveralls-maven-plugin - bintray - https://dl.bintray.com/jwtk/coveralls-maven-plugin - - - - - - false - - bintray-jwtk-coveralls-maven-plugin - bintray-plugins - https://dl.bintray.com/jwtk/coveralls-maven-plugin - - UTF-8 - + ${basedir} - 0.10.7 + 0.11.2 3.2.2 3.8.1 @@ -123,6 +104,8 @@ 1.6 0.13.1 1.6.1 + 4.2.rc3 + true 1.7 ${user.name}-${maven.build.timestamp} @@ -131,7 +114,7 @@ 20230227 2.9.0 - + 1.70 @@ -145,7 +128,32 @@ 3.0.0-M5 4.3.1 ${jjwt.root}/target/clover/clover.db - + + + --add-opens java.base/java.time.zone=ALL-UNNAMED, + --add-opens java.base/java.lang.ref=ALL-UNNAMED, + --add-opens java.base/java.lang=ALL-UNNAMED, + --add-opens java.base/java.lang.reflect=ALL-UNNAMED, + --add-opens java.base/java.net=ALL-UNNAMED, + --add-opens java.base/java.nio.charset=ALL-UNNAMED, + --add-opens java.base/java.nio.file=ALL-UNNAMED, + --add-opens java.base/java.security.cert=ALL-UNNAMED, + --add-opens java.base/java.text=ALL-UNNAMED, + --add-opens java.base/java.util.regex=ALL-UNNAMED, + --add-opens java.base/java.util.stream=ALL-UNNAMED, + --add-opens java.base/java.util.concurrent=ALL-UNNAMED, + --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED, + --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED, + --add-opens java.base/java.time=ALL-UNNAMED, + --add-opens java.base/java.util=ALL-UNNAMED, + --add-opens java.base/java.io=ALL-UNNAMED, + --add-opens java.base/java.security=ALL-UNNAMED, + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED, + --add-opens java.base/sun.nio.fs=ALL-UNNAMED, + --add-opens java.base/sun.security.x509=ALL-UNNAMED, + --add-opens java.base/sun.security.util=ALL-UNNAMED, + --add-opens java.logging/java.util.logging=ALL-UNNAMED + @@ -287,6 +295,7 @@ ${user.home}/.clover/${project.groupId}/jjwt --> io/jsonwebtoken/lang/* + io/jsonwebtoken/all/JavaReadmeTest.java 100.000000% 100.000000% @@ -297,26 +306,44 @@ com.mycila license-maven-plugin - 3.0 + ${maven.license.version} - true - true -
    ${jjwt.root}/src/license/header.txt
    - - ${jjwt.root}/src/license/header_format.xml - - - ${project.organization.name} - 2019 - - - **/*.txt - LICENSE - **/lombok.config - **/mvnw - .gitattributes - + ${maven.license.skipExistingHeaders} + + SCRIPT_STYLE + SCRIPT_STYLE + + + +
    ${jjwt.root}/src/license/header.txt
    + + **/license/header.txt + **/*.test.orgjson + **/*.test.gson + **/*.test.override + **/*.bnd + LICENSE + **/mvnw + **/lombok.config + .gitattributes + +
    +
    + + + com.mycila + license-maven-plugin-git + ${maven.license.version} + + + + + + check + + +
    org.apache.maven.plugins @@ -394,10 +421,18 @@ true - true - + - + true + + + @@ -428,7 +463,8 @@ - + @@ -592,7 +628,6 @@ - io.jsonwebtoken.coveralls coveralls-maven-plugin 4.4.1 @@ -609,7 +644,7 @@ 1.13.1 3.0.10 4.2 - 2.0.2 + 2.0.7 0.15.6 @@ -621,21 +656,17 @@ -html5 - --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + ${test.addOpens}, --illegal-access=debug - jdk16AndLater + jdk17AndLater - [16,) + [17,) - - --add-opens java.base/java.lang=ALL-UNNAMED - --add-opens java.base/java.lang.ref=ALL-UNNAMED - --add-opens java.base/java.util=ALL-UNNAMED - --add-opens java.base/sun.security.util=ALL-UNNAMED - + -html5 + ${test.addOpens} diff --git a/src/license/header.txt b/src/license/header.txt index 07a81f4c..ccc8b958 100644 --- a/src/license/header.txt +++ b/src/license/header.txt @@ -1,4 +1,4 @@ -Copyright (C) ${year} ${organization.name} +Copyright © ${license.git.copyrightCreationYear} ${project.organization.name} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/license/header_format.xml b/src/license/header_format.xml deleted file mode 100644 index 9ce0b82b..00000000 --- a/src/license/header_format.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - ~ - ]]> - $]]> - - (\s|\t)*$]]> - false - true - false - - - /* - * - */ - (\s|\t)*/\*.*$ - .*\*/(\s|\t)*$ - false - true - false - - \ No newline at end of file diff --git a/tdjar/pom.xml b/tdjar/pom.xml index 1533cbdf..112deb7e 100644 --- a/tdjar/pom.xml +++ b/tdjar/pom.xml @@ -49,5 +49,17 @@ jjwt-jackson runtime + + + + org.bouncycastle + bcprov-jdk15on + test + + + org.bouncycastle + bcpkix-jdk15on + test +
    diff --git a/tdjar/src/test/groovy/io/jsonwebtoken/all/BasicTest.groovy b/tdjar/src/test/groovy/io/jsonwebtoken/all/BasicTest.groovy index e81f813e..bf1ffaa3 100644 --- a/tdjar/src/test/groovy/io/jsonwebtoken/all/BasicTest.groovy +++ b/tdjar/src/test/groovy/io/jsonwebtoken/all/BasicTest.groovy @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 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.all import io.jsonwebtoken.* diff --git a/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java b/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java new file mode 100644 index 00000000..aaf76702 --- /dev/null +++ b/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2022 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.all; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.jackson.io.JacksonSerializer; +import io.jsonwebtoken.security.AeadAlgorithm; +import io.jsonwebtoken.security.EcPrivateJwk; +import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.Jwks; +import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.MacAlgorithm; +import io.jsonwebtoken.security.OctetPrivateJwk; +import io.jsonwebtoken.security.OctetPublicJwk; +import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.RsaPrivateJwk; +import io.jsonwebtoken.security.RsaPublicJwk; +import io.jsonwebtoken.security.SecretJwk; +import io.jsonwebtoken.security.SecretKeyAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithm; +import org.junit.Test; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import static io.jsonwebtoken.security.Jwks.builder; + +/** + * Test cases to ensure snippets in README.md work/compile as expected with the Java language (not Groovy): + * + * @since JJWT_RELEASE_VERSION + */ +public class JavaReadmeTest { + + /** + * {@code README.md#example-jws-hs} + */ + @Test + public void testExampleJwsHS() { + // Create a test key suitable for the desired HMAC-SHA algorithm: + MacAlgorithm alg = Jwts.SIG.HS512; //or HS384 or HS256 + SecretKey key = alg.keyBuilder().build(); + + String message = "Hello World!"; + byte[] content = message.getBytes(StandardCharsets.UTF_8); + + // Create the compact JWS: + String jws = Jwts.builder().setContent(content, "text/plain").signWith(key, alg).compact(); + + // Parse the compact JWS: + content = Jwts.parserBuilder().verifyWith(key).build().parseContentJws(jws).getPayload(); + + assert message.equals(new String(content, StandardCharsets.UTF_8)); + } + + /** + * {@code README.md#example-jws-rsa} + */ + @Test + public void testExampleJwsRSA() { + // Create a test key suitable for the desired RSA signature algorithm: + SignatureAlgorithm alg = Jwts.SIG.RS512; //or PS512, RS256, etc... + KeyPair pair = alg.keyPairBuilder().build(); + + // Bob creates the compact JWS with his RSA private key: + String jws = Jwts.builder().setSubject("Alice") + .signWith(pair.getPrivate(), alg) // <-- Bob's RSA private key + .compact(); + + // Alice receives and verifies the compact JWS came from Bob: + String subject = Jwts.parserBuilder() + .verifyWith(pair.getPublic()) // <-- Bob's RSA public key + .build().parseClaimsJws(jws).getPayload().getSubject(); + + assert "Alice".equals(subject); + } + + /** + * {@code README.md#example-jws-ecdsa} + */ + @Test + public void testExampleJwsECDSA() { + // Create a test key suitable for the desired ECDSA signature algorithm: + SignatureAlgorithm alg = Jwts.SIG.ES512; //or ES256 or ES384 + KeyPair pair = alg.keyPairBuilder().build(); + + // Bob creates the compact JWS with his EC private key: + String jws = Jwts.builder().setSubject("Alice") + .signWith(pair.getPrivate(), alg) // <-- Bob's EC private key + .compact(); + + // Alice receives and verifies the compact JWS came from Bob: + String subject = Jwts.parserBuilder() + .verifyWith(pair.getPublic()) // <-- Bob's EC public key + .build().parseClaimsJws(jws).getPayload().getSubject(); + + assert "Alice".equals(subject); + } + + /** + * {@code README.md#example-jwe-dir} + */ + @Test + public void testExampleJweDir() { + // Create a test key suitable for the desired payload encryption algorithm: + // (A*GCM algorithms are recommended, but require JDK 8 or later) + AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A128GCM, A192GCM, A256CBC-HS512, etc... + SecretKey key = enc.keyBuilder().build(); + + String message = "Live long and prosper."; + byte[] content = message.getBytes(StandardCharsets.UTF_8); + + // Create the compact JWE: + String jwe = Jwts.builder().setContent(content, "text/plain").encryptWith(key, enc).compact(); + + // Parse the compact JWE: + content = Jwts.parserBuilder().decryptWith(key).build().parseContentJwe(jwe).getPayload(); + + assert message.equals(new String(content, StandardCharsets.UTF_8)); + } + + /** + * {@code README.md#example-jwe-rsa} + */ + @Test + public void testExampleJweRSA() { + // Create a test KeyPair suitable for the desired RSA key algorithm: + KeyPair pair = Jwts.SIG.RS512.keyPairBuilder().build(); + + // Choose the key algorithm used encrypt the payload key: + KeyAlgorithm alg = Jwts.KEY.RSA_OAEP_256; //or RSA_OAEP or RSA1_5 + // Choose the Encryption Algorithm to encrypt the payload: + AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + + // Bob creates the compact JWE with Alice's RSA public key so only she may read it: + String jwe = Jwts.builder().setAudience("Alice") + .encryptWith(pair.getPublic(), alg, enc) // <-- Alice's RSA public key + .compact(); + + // Alice receives and decrypts the compact JWE: + String audience = Jwts.parserBuilder() + .decryptWith(pair.getPrivate()) // <-- Alice's RSA private key + .build().parseClaimsJwe(jwe).getPayload().getAudience(); + + assert "Alice".equals(audience); + } + + /** + * {@code README.md#example-jwe-aeskw} + */ + @Test + public void testExampleJweAESKW() { + // Create a test SecretKey suitable for the desired AES Key Wrap algorithm: + SecretKeyAlgorithm alg = Jwts.KEY.A256GCMKW; //or A192GCMKW, A128GCMKW, A256KW, etc... + SecretKey key = alg.keyBuilder().build(); + + // Chooose the Encryption Algorithm used to encrypt the payload: + AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + + // Create the compact JWE: + String jwe = Jwts.builder().setIssuer("me").encryptWith(key, alg, enc).compact(); + + // Parse the compact JWE: + String issuer = Jwts.parserBuilder().decryptWith(key).build() + .parseClaimsJwe(jwe).getPayload().getIssuer(); + + assert "me".equals(issuer); + } + + /** + * {@code README.md#example-jwe-ecdhes} + */ + @Test + public void testExampleJweECDHES() { + // Create a test KeyPair suitable for the desired EC key algorithm: + KeyPair pair = Jwts.SIG.ES512.keyPairBuilder().build(); + + // Choose the key algorithm used encrypt the payload key: + KeyAlgorithm alg = Jwts.KEY.ECDH_ES_A256KW; //ECDH_ES_A192KW, etc... + // Choose the Encryption Algorithm to encrypt the payload: + AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + + // Bob creates the compact JWE with Alice's EC public key so only she may read it: + String jwe = Jwts.builder().setAudience("Alice") + .encryptWith(pair.getPublic(), alg, enc) // <-- Alice's EC public key + .compact(); + + // Alice receives and decrypts the compact JWE: + String audience = Jwts.parserBuilder() + .decryptWith(pair.getPrivate()) // <-- Alice's EC private key + .build().parseClaimsJwe(jwe).getPayload().getAudience(); + + assert "Alice".equals(audience); + } + + /** + * {@code README.md#example-jwe-password} + */ + @Test + public void testExampleJwePassword() { + //DO NOT use this example password in a real app, it is well-known to password crackers + String pw = "correct horse battery staple"; + Password password = Keys.forPassword(pw.toCharArray()); + + // Choose the desired PBES2 key derivation algorithm: + KeyAlgorithm alg = Jwts.KEY.PBES2_HS512_A256KW; //or PBES2_HS384... + + // Optionally choose the number of PBES2 computational iterations to use to derive the key. + // This is optional - if you do not specify a value, JJWT will automatically choose a value + // based on your chosen PBES2 algorithm and OWASP PBKDF2 recommendations here: + // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 + // + // If you do specify a value, ensure the iterations are large enough for your desired alg + //int pbkdf2Iterations = 120000; //for HS512. Needs to be much higher for smaller hash algs. + + // Choose the Encryption Algorithm used to encrypt the payload: + AeadAlgorithm enc = Jwts.ENC.A256GCM; //or A192GCM, A128GCM, A256CBC-HS512, etc... + + // Create the compact JWE: + String jwe = Jwts.builder().setIssuer("me") + // Optional work factor is specified in the header: + //.setHeader(Jwts.headerBuilder().setPbes2Count(pbkdf2Iterations)) + .encryptWith(password, alg, enc) + .compact(); + + // Parse the compact JWE: + String issuer = Jwts.parserBuilder().decryptWith(password) + .build().parseClaimsJwe(jwe).getPayload().getIssuer(); + + assert "me".equals(issuer); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleSecretJwk() { + SecretKey key = Jwts.SIG.HS512.keyBuilder().build(); // or HS384 or HS256 + SecretJwk jwk = builder().forKey(key).setIdFromThumbprint().build(); + + assert jwk.getId().equals(jwk.thumbprint().toString()); + assert key.equals(jwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof SecretJwk; + assert jwk.equals(parsed); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleRsaPublicJwk() { + RSAPublicKey key = (RSAPublicKey) Jwts.SIG.RS512.keyPairBuilder().build().getPublic(); + RsaPublicJwk jwk = builder().forKey(key).setIdFromThumbprint().build(); + + assert jwk.getId().equals(jwk.thumbprint().toString()); + assert key.equals(jwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof RsaPublicJwk; + assert jwk.equals(parsed); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleRsaPrivateJwk() { + KeyPair pair = Jwts.SIG.RS512.keyPairBuilder().build(); + RSAPublicKey pubKey = (RSAPublicKey) pair.getPublic(); + RSAPrivateKey privKey = (RSAPrivateKey) pair.getPrivate(); + + RsaPrivateJwk privJwk = builder().forKey(privKey).setIdFromThumbprint().build(); + RsaPublicJwk pubJwk = privJwk.toPublicJwk(); + + assert privJwk.getId().equals(privJwk.thumbprint().toString()); + assert pubJwk.getId().equals(pubJwk.thumbprint().toString()); + assert privKey.equals(privJwk.toKey()); + assert pubKey.equals(pubJwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(privJwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof RsaPrivateJwk; + assert privJwk.equals(parsed); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleEcPublicJwk() { + ECPublicKey key = (ECPublicKey) Jwts.SIG.ES512.keyPairBuilder().build().getPublic(); + EcPublicJwk jwk = builder().forKey(key).setIdFromThumbprint().build(); + + assert jwk.getId().equals(jwk.thumbprint().toString()); + assert key.equals(jwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof EcPublicJwk; + assert jwk.equals(parsed); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleEcPrivateJwk() { + KeyPair pair = Jwts.SIG.ES512.keyPairBuilder().build(); + ECPublicKey pubKey = (ECPublicKey) pair.getPublic(); + ECPrivateKey privKey = (ECPrivateKey) pair.getPrivate(); + + EcPrivateJwk privJwk = builder().forKey(privKey).setIdFromThumbprint().build(); + EcPublicJwk pubJwk = privJwk.toPublicJwk(); + + assert privJwk.getId().equals(privJwk.thumbprint().toString()); + assert pubJwk.getId().equals(pubJwk.thumbprint().toString()); + assert privKey.equals(privJwk.toKey()); + assert pubKey.equals(pubJwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(privJwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof EcPrivateJwk; + assert privJwk.equals(parsed); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleEdEcPublicJwk() { + PublicKey key = Jwts.SIG.Ed25519.keyPairBuilder().build().getPublic(); + OctetPublicJwk jwk = builder().forOctetKey(key).setIdFromThumbprint().build(); + + assert jwk.getId().equals(jwk.thumbprint().toString()); + assert key.equals(jwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(jwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof OctetPublicJwk; + assert jwk.equals(parsed); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testExampleEdEcPrivateJwk() { + KeyPair pair = Jwts.SIG.Ed448.keyPairBuilder().build(); + PublicKey pubKey = pair.getPublic(); + PrivateKey privKey = pair.getPrivate(); + + OctetPrivateJwk privJwk = builder().forOctetKey(privKey).setIdFromThumbprint().build(); + OctetPublicJwk pubJwk = privJwk.toPublicJwk(); + + assert privJwk.getId().equals(privJwk.thumbprint().toString()); + assert pubJwk.getId().equals(pubJwk.thumbprint().toString()); + assert privKey.equals(privJwk.toKey()); + assert pubKey.equals(pubJwk.toKey()); + + byte[] utf8Bytes = new JacksonSerializer().serialize(privJwk); // or GsonSerializer(), etc + String jwkJson = new String(utf8Bytes, StandardCharsets.UTF_8); + Jwk parsed = Jwks.parser().build().parse(jwkJson); + + assert parsed instanceof OctetPrivateJwk; + assert privJwk.equals(parsed); + } + + @Test + public void testExampleJwkToString() { + String json = "{\"kty\":\"oct\"," + + "\"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"," + + "\"kid\":\"HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example.\"}"; + + Jwk jwk = Jwks.parser().build().parse(json); + + String expected = "{kty=oct, k=, kid=HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example.}"; + assert expected.equals(jwk.toString()); + } +}

    Per RFC 7518, Section 3.4: + * + * the Integer-to-OctetString Conversion + * defined in Section 2.3.7 of SEC1 [SEC1] used to represent R and S as + * octet sequences adds zero-valued high-order padding bits when needed + * to round the size up to a multiple of 8 bits; thus, each 521-bit + * integer is represented using 528 bits in 66 octets. + * + *