From 23ef0333a3aac011d3046a6a945b273441fc938e Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:16:19 -0700 Subject: [PATCH] password hashing / iteration estimate algorithm / impl checkpoint --- .../main/java/io/jsonwebtoken/JweBuilder.java | 3 - .../main/java/io/jsonwebtoken/JwtBuilder.java | 6 +- .../io/jsonwebtoken/JwtParserBuilder.java | 2 +- .../io/jsonwebtoken/security/AeadResult.java | 2 +- ...ource.java => AssociatedDataSupplier.java} | 2 +- ...tionTagSource.java => DigestSupplier.java} | 4 +- .../jsonwebtoken/security/EcKeyAlgorithm.java | 8 + .../security/EncryptedKeyAlgorithm.java | 17 -- .../security/EncryptionAlgorithmLocator.java | 11 -- .../jsonwebtoken/security/HashAlgorithm.java | 6 - .../jsonwebtoken/security/HashAlgorithms.java | 20 -- ...java => InitializationVectorSupplier.java} | 2 +- .../jsonwebtoken/security/KeyAlgorithm.java | 20 +- .../jsonwebtoken/security/KeyAlgorithms.java | 36 ++-- .../io/jsonwebtoken/security/KeyRequest.java | 2 + .../io/jsonwebtoken/security/KeyResolver.java | 13 -- .../io/jsonwebtoken/security/KeyResult.java | 3 - .../java/io/jsonwebtoken/security/Keys.java | 18 +- .../jsonwebtoken/security/LocatorAdapter.java | 8 +- .../java/io/jsonwebtoken/security/PbeKey.java | 11 ++ .../jsonwebtoken/security/PbeKeyBuilder.java | 16 ++ .../security/RsaKeyAlgorithm.java | 8 + .../security/SymmetricAeadAlgorithm.java | 4 +- .../SymmetricAeadDecryptionRequest.java | 2 +- .../security/SymmetricAeadRequest.java | 2 +- .../security/VerifySignatureRequest.java | 4 +- .../jsonwebtoken/impl/DefaultJweBuilder.java | 27 ++- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 6 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 2 +- .../impl/DefaultJwtParserBuilder.java | 2 +- .../java/io/jsonwebtoken/impl/lang/Bytes.java | 10 +- .../io/jsonwebtoken/impl/lang/Supplier.java | 6 - .../jsonwebtoken/impl/lang/ValueGetter.java | 18 ++ .../impl/security/AbstractEcJwkFactory.java | 41 +++-- .../security/AbstractFamilyJwkFactory.java | 36 +--- .../impl/security/AbstractJwk.java | 15 +- .../security/AbstractSecurityRequest.java | 27 +++ .../security/AbstractSignatureAlgorithm.java | 4 +- .../impl/security/AesAlgorithm.java | 66 +++---- .../impl/security/AesGcmKeyAlgorithm.java | 72 +++----- .../impl/security/AesWrapKeyAlgorithm.java | 20 +- .../jsonwebtoken/impl/security/ConcatKDF.java | 18 +- .../impl/security/CryptoAlgorithm.java | 12 +- .../impl/security/DefaultAeadResult.java | 4 +- .../impl/security/DefaultCryptoRequest.java | 4 +- ...efaultEllipticCurveSignatureAlgorithm.java | 15 +- .../impl/security/DefaultJweFactory.java | 123 ------------- .../impl/security/DefaultJwkContext.java | 93 +++++----- .../impl/security/DefaultKeyRequest.java | 20 +- .../impl/security/DefaultKeyResult.java | 22 +-- .../impl/security/DefaultPBEKey.java | 91 --------- .../impl/security/DefaultPayloadSupplier.java | 15 +- .../impl/security/DefaultPbeKey.java | 104 +++++++++++ .../impl/security/DefaultPbeKeyBuilder.java | 96 ++++++++++ .../impl/security/DefaultRsaKeyAlgorithm.java | 29 +-- .../DefaultRsaSignatureAlgorithm.java | 13 +- .../impl/security/DefaultSecurityRequest.java | 27 +++ .../security/DefaultSymmetricAeadRequest.java | 4 +- .../impl/security/DefaultValueGetter.java | 149 +++++++++++++++ .../DefaultVerifySignatureRequest.java | 2 +- .../impl/security/EcPrivateJwkFactory.java | 6 +- .../impl/security/EcPublicJwkFactory.java | 8 +- .../impl/security/EncryptionAlgorithm.java | 22 --- .../security/EncryptionAlgorithmsBridge.java | 2 +- .../impl/security/GcmAesAeadAlgorithm.java | 18 +- .../impl/security/HmacAesAeadAlgorithm.java | 15 +- .../impl/security/InstanceCallback.java | 6 - .../jsonwebtoken/impl/security/JcaPbeKey.java | 51 ++++++ .../impl/security/JcaTemplate.java | 27 +-- .../impl/security/JwkContext.java | 25 +-- .../impl/security/KeyAlgorithmsBridge.java | 162 +++++++++++++++++ .../impl/security/KeyedRequest.java | 24 +++ .../impl/security/KeysBridge.java | 16 ++ .../impl/security/MacSignatureAlgorithm.java | 5 +- .../impl/security/Pbes2HsAkwAlgorithm.java | 172 ++++++++---------- .../impl/security/RsaPrivateJwkFactory.java | 21 ++- .../impl/security/RsaPublicJwkFactory.java | 7 +- .../impl/security/SecretJwkFactory.java | 17 +- .../impl/DefaultJweBuilderTest.groovy | 10 +- .../security/AesGcmKeyAlgorithmTest.groovy | 57 +++--- .../security/DefaultJweFactoryTest.groovy | 14 -- .../security/DirectKeyAlgorithmTest.groovy | 8 +- .../security/GcmAesAeadAlgorithmTest.groovy | 2 +- .../security/HmacAesAeadAlgorithmTest.groovy | 2 +- .../impl/security/JcaTemplateTest.groovy | 9 +- .../security/Pbes2HsAkwAlgorithmTest.groovy | 44 +++++ .../impl/security/RFC7517AppendixCTest.groovy | 7 +- .../security/RFC7518AppendixB1Test.groovy | 2 +- .../security/RFC7518AppendixB2Test.groovy | 2 +- .../security/RFC7518AppendixB3Test.groovy | 2 +- .../security/EncryptionAlgorithmsTest.groovy | 4 +- 91 files changed, 1272 insertions(+), 918 deletions(-) rename api/src/main/java/io/jsonwebtoken/security/{AssociatedDataSource.java => AssociatedDataSupplier.java} (94%) rename api/src/main/java/io/jsonwebtoken/security/{AuthenticationTagSource.java => DigestSupplier.java} (89%) create mode 100644 api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptedKeyAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java delete mode 100644 api/src/main/java/io/jsonwebtoken/security/HashAlgorithms.java rename api/src/main/java/io/jsonwebtoken/security/{InitializationVectorSource.java => InitializationVectorSupplier.java} (89%) delete mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyResolver.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PbeKey.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityRequest.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPBEKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithm.java delete mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/InstanceCallback.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeyedRequest.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy diff --git a/api/src/main/java/io/jsonwebtoken/JweBuilder.java b/api/src/main/java/io/jsonwebtoken/JweBuilder.java index 8650b84b..f70097f1 100644 --- a/api/src/main/java/io/jsonwebtoken/JweBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JweBuilder.java @@ -1,6 +1,5 @@ package io.jsonwebtoken; -import io.jsonwebtoken.security.EncryptedKeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.SymmetricAeadAlgorithm; @@ -17,6 +16,4 @@ public interface JweBuilder extends JwtBuilder { JweBuilder withKey(SecretKey key); JweBuilder withKeyFrom(K key, KeyAlgorithm alg); - - JweBuilder withKeyFrom(char[] password, int iterations, EncryptedKeyAlgorithm alg); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 0b05ec2f..f0810fa4 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -74,7 +74,7 @@ public interface JwtBuilder> extends ClaimsMutator { * @param header the header to set (and potentially replace any existing header). * @return the builder for method chaining. */ - T setHeader(Map header); + T setHeader(Map header); /** * Applies the specified name/value pairs to the header. If a header does not yet exist at the time this method @@ -83,7 +83,7 @@ public interface JwtBuilder> extends ClaimsMutator { * @param params the header name/value pairs to append to the header. * @return the builder for method chaining. */ - T setHeaderParams(Map params); + T setHeaderParams(Map params); //sets the specified header parameter, overwriting any previous value under the same name. @@ -141,7 +141,7 @@ public interface JwtBuilder> extends ClaimsMutator { * @return the builder for method chaining. * @since 0.8 */ - T addClaims(Map claims); + T addClaims(Map claims); /** * Sets the JWT Claims diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 9cc231e6..44890e5b 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -293,7 +293,7 @@ public interface JwtParserBuilder { * .parseClaimsJws(compact); * *

- *

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

+ *

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. diff --git a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java index 2feb4f2b..3cb1b0e3 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AeadResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/AeadResult.java @@ -3,5 +3,5 @@ package io.jsonwebtoken.security; /** * @since JJWT_RELEASE_VERSION */ -public interface AeadResult extends PayloadSupplier, AuthenticationTagSource, InitializationVectorSource { +public interface AeadResult extends PayloadSupplier, DigestSupplier, InitializationVectorSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java similarity index 94% rename from api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java rename to api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java index 0acd5555..b9483def 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSource.java +++ b/api/src/main/java/io/jsonwebtoken/security/AssociatedDataSupplier.java @@ -18,7 +18,7 @@ package io.jsonwebtoken.security; /** * @since JJWT_RELEASE_VERSION */ -public interface AssociatedDataSource { +public interface AssociatedDataSupplier { byte[] getAssociatedData(); diff --git a/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java similarity index 89% rename from api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java rename to api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java index b7e1f55e..c1499132 100644 --- a/api/src/main/java/io/jsonwebtoken/security/AuthenticationTagSource.java +++ b/api/src/main/java/io/jsonwebtoken/security/DigestSupplier.java @@ -18,8 +18,8 @@ package io.jsonwebtoken.security; /** * @since JJWT_RELEASE_VERSION */ -public interface AuthenticationTagSource { +public interface DigestSupplier { - byte[] getAuthenticationTag(); + byte[] getDigest(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java new file mode 100644 index 00000000..c6f23873 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/EcKeyAlgorithm.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECKey; + +public interface EcKeyAlgorithm extends KeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptedKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/EncryptedKeyAlgorithm.java deleted file mode 100644 index ab280313..00000000 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptedKeyAlgorithm.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.jsonwebtoken.security; - -import java.security.Key; - -/** - * A {@link KeyAlgorithm} that produces an encrypted key value. {@code EncryptedKeyAlgorithm}s will be supplied - * a secure-randomly-generated Content Encryption Key in the request's {@link CryptoRequest#getPayload() getData()} method. - * - *

- * A {@code KeyAlgorithm} that does not produce an encrypted key value (or produces an empty key byte array) should - * not implement this interface, and instead implement the {@code KeyAlgorithm} parent interface directly.

- * - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptedKeyAlgorithm extends KeyAlgorithm { - -} diff --git a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java b/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java deleted file mode 100644 index 0e341f0c..00000000 --- a/api/src/main/java/io/jsonwebtoken/security/EncryptionAlgorithmLocator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.JweHeader; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptionAlgorithmLocator { - - SymmetricAeadAlgorithm getEncryptionAlgorithm(JweHeader jweHeader); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java deleted file mode 100644 index 03ce7824..00000000 --- a/api/src/main/java/io/jsonwebtoken/security/HashAlgorithm.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.Identifiable; - -public interface HashAlgorithm extends Identifiable { -} diff --git a/api/src/main/java/io/jsonwebtoken/security/HashAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/HashAlgorithms.java deleted file mode 100644 index 179d4fb6..00000000 --- a/api/src/main/java/io/jsonwebtoken/security/HashAlgorithms.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.jsonwebtoken.security; - -public final class HashAlgorithms { - - //prevent instantiation - private HashAlgorithms() {} - - private static HashAlgorithm forJcaName(final String jcaName) { - //TODO: IMPLEMENT ME - return new HashAlgorithm() { - @Override - public String getId() { - return jcaName; - } - }; - } - public static final HashAlgorithm MD5 = forJcaName("MD5"); - public static final HashAlgorithm SHA_1 = forJcaName("SHA-1"); - public static final HashAlgorithm SHA_256 = forJcaName("SHA-256"); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java similarity index 89% rename from api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java rename to api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java index 47f1032e..757d3278 100644 --- a/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSource.java +++ b/api/src/main/java/io/jsonwebtoken/security/InitializationVectorSupplier.java @@ -3,7 +3,7 @@ package io.jsonwebtoken.security; /** * @since JJWT_RELEASE_VERSION */ -public interface InitializationVectorSource { +public interface InitializationVectorSupplier { /** * Returns the secure-random initialization vector used during encryption that must be presented in order diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java index c8ce96b1..9e328cf8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithm.java @@ -6,21 +6,15 @@ import javax.crypto.SecretKey; import java.security.Key; /** - * A
Key Management Algorithm is an algorithm that - * produces a {@link SecretKey} used to encrypt or decrypt a JWE. The Key Management Algorithm used for a particular - * JWE is {@link #getId() identified} in the - * JWE's {@code alg} header. - *

Key Management Mode

- * The JWE specification indicates that all {@code Key Management Algorithm}s utilize what is called a - * {@code Key Management Mode} to indicate if the encryption key will be supplied to a JWE recipient within the - * JWE as an encrypted value. This interface does not indicate which {@code Key Management Mode} is used: - * the {@link #getEncryptionKey(KeyRequest) key result} may contain either an empty or populated encrypted key via - * {@link KeyResult#getPayload() result.getPayload()}. - *

Therefore, algorithms that produce encrypted keys MUST implement the - * {@link EncryptedKeyAlgorithm} interface instead of this one.

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

* * @since JJWT_RELEASE_VERSION - * @see EncryptedKeyAlgorithm + * @see RFC 7561, Section 2: JWE Key (Management) Algorithms */ public interface KeyAlgorithm extends Identifiable { diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java index cda15ed4..6e5bb66b 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyAlgorithms.java @@ -4,8 +4,6 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import javax.crypto.SecretKey; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; import java.util.Collection; /** @@ -19,8 +17,9 @@ public final class KeyAlgorithms { private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeyAlgorithmsBridge"; private static final Class[] ID_ARG_TYPES = new Class[]{String.class}; + private static final Class[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class}; - public static Collection values() { + public static Collection> values() { return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null); } @@ -49,16 +48,23 @@ public final class KeyAlgorithms { } public static final KeyAlgorithm DIRECT = forId0("dir"); - public static final EncryptedKeyAlgorithm A128KW = forId0("A128KW"); - public static final EncryptedKeyAlgorithm A192KW = forId0("A192KW"); - public static final EncryptedKeyAlgorithm A256KW = forId0("A256KW"); - public static final EncryptedKeyAlgorithm A128GCMKW = forId0("A128GCMKW"); - public static final EncryptedKeyAlgorithm A192GCMKW = forId0("A192GCMKW"); - public static final EncryptedKeyAlgorithm A256GCMKW = forId0("A256GCMKW"); - public static final EncryptedKeyAlgorithm RSA1_5 = forId0("RSA1_5"); - public static final EncryptedKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); - public static final EncryptedKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); - public static final EncryptedKeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); - public static final EncryptedKeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); - public static final EncryptedKeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); + public static final KeyAlgorithm A128KW = forId0("A128KW"); + public static final KeyAlgorithm A192KW = forId0("A192KW"); + public static final KeyAlgorithm A256KW = forId0("A256KW"); + public static final KeyAlgorithm A128GCMKW = forId0("A128GCMKW"); + public static final KeyAlgorithm A192GCMKW = forId0("A192GCMKW"); + public static final KeyAlgorithm A256GCMKW = forId0("A256GCMKW"); + @SuppressWarnings("rawtypes") + public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5"); + @SuppressWarnings("rawtypes") + public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP"); + @SuppressWarnings("rawtypes") + public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256"); + public static final KeyAlgorithm PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); + public static final KeyAlgorithm PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); + public static final KeyAlgorithm PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); + + public static int estimateIterations(KeyAlgorithm alg, long desiredMillis) { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java index 6c74c254..73d9dfcf 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyRequest.java @@ -9,5 +9,7 @@ import java.security.Key; */ public interface KeyRequest extends CryptoRequest { + SymmetricAeadAlgorithm getEncryptionAlgorithm(); + JweHeader getHeader(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyResolver.java b/api/src/main/java/io/jsonwebtoken/security/KeyResolver.java deleted file mode 100644 index b1646d1b..00000000 --- a/api/src/main/java/io/jsonwebtoken/security/KeyResolver.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jsonwebtoken.security; - -import io.jsonwebtoken.Header; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface KeyResolver { - - Key resolveKey(Header header); -} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java index d5c1ddeb..228b78be 100644 --- a/api/src/main/java/io/jsonwebtoken/security/KeyResult.java +++ b/api/src/main/java/io/jsonwebtoken/security/KeyResult.java @@ -1,12 +1,9 @@ package io.jsonwebtoken.security; import javax.crypto.SecretKey; -import java.util.Map; /** * @since JJWT_RELEASE_VERSION */ public interface KeyResult extends PayloadSupplier, KeySupplier { - - Map getHeaderParams(); } diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index 57a5f877..8fa4621e 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -16,8 +16,10 @@ package io.jsonwebtoken.security; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Classes; import javax.crypto.SecretKey; +import javax.crypto.interfaces.PBEKey; import javax.crypto.spec.SecretKeySpec; import java.security.KeyPair; @@ -28,6 +30,8 @@ import java.security.KeyPair; */ public final class Keys { + private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge"; + //prevent instantiation private Keys() { } @@ -115,7 +119,7 @@ public final class Keys { @Deprecated public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - SignatureAlgorithm salg = SignatureAlgorithms.forId(alg.name()); + SignatureAlgorithm salg = SignatureAlgorithms.forId(alg.name()); if (!(salg instanceof SecretKeySignatureAlgorithm)) { String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; throw new IllegalArgumentException(msg); @@ -210,12 +214,20 @@ public final class Keys { @Deprecated public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - SignatureAlgorithm salg = SignatureAlgorithms.forId(alg.name()); + SignatureAlgorithm salg = SignatureAlgorithms.forId(alg.name()); if (!(salg instanceof AsymmetricKeySignatureAlgorithm)) { String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; throw new IllegalArgumentException(msg); } - AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); + AsymmetricKeySignatureAlgorithm asalg = ((AsymmetricKeySignatureAlgorithm) salg); return asalg.generateKeyPair(); } + + public static PbeKey toPbeKey(PBEKey key) { + return forPbe().forKey(key).build(); + } + + public static PbeKeyBuilder forPbe() { + return Classes.invokeStatic(BRIDGE_CLASSNAME, "forPbe", null, (Object[]) null); + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/LocatorAdapter.java b/api/src/main/java/io/jsonwebtoken/security/LocatorAdapter.java index 8fdfc135..89ba9bf8 100644 --- a/api/src/main/java/io/jsonwebtoken/security/LocatorAdapter.java +++ b/api/src/main/java/io/jsonwebtoken/security/LocatorAdapter.java @@ -15,9 +15,9 @@ public abstract class LocatorAdapter, R> implements Locator< return locate((JwsHeader) header); } else if (header instanceof JweHeader) { return locate((JweHeader) header); + } else { + return doLocate(header); } - String msg = "Unrecognized header type: " + header.getClass().getName(); - throw new IllegalStateException(msg); } protected R locate(JweHeader header) { @@ -27,4 +27,8 @@ public abstract class LocatorAdapter, R> implements Locator< protected R locate(JwsHeader header) { return null; } + + protected R doLocate(Header header) { + return null; + } } diff --git a/api/src/main/java/io/jsonwebtoken/security/PbeKey.java b/api/src/main/java/io/jsonwebtoken/security/PbeKey.java new file mode 100644 index 00000000..a5c81372 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PbeKey.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +import javax.crypto.SecretKey; + +public interface PbeKey extends SecretKey { + + char[] getPassword(); + + int getWorkFactor(); + +} diff --git a/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java new file mode 100644 index 00000000..e2f59431 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PbeKeyBuilder.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.security; + +import javax.crypto.interfaces.PBEKey; + +public interface PbeKeyBuilder { + + PbeKeyBuilder forKey(PBEKey jcaKey); + + PbeKeyBuilder setPassword(String password); + + PbeKeyBuilder setPassword(char[] password); + + PbeKeyBuilder setWorkFactor(int workFactor); + + K build(); +} diff --git a/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java new file mode 100644 index 00000000..3aa0441e --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/RsaKeyAlgorithm.java @@ -0,0 +1,8 @@ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAKey; + +public interface RsaKeyAlgorithm extends KeyAlgorithm { +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadAlgorithm.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadAlgorithm.java index 7cf4220e..5bf0f2bb 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadAlgorithm.java @@ -7,7 +7,7 @@ import io.jsonwebtoken.Identifiable; */ public interface SymmetricAeadAlgorithm extends Identifiable, SecretKeyGenerator { - AeadResult encrypt(SymmetricAeadRequest request) throws CryptoException, KeyException; + AeadResult encrypt(SymmetricAeadRequest request) throws SecurityException; - PayloadSupplier decrypt(SymmetricAeadDecryptionRequest request) throws CryptoException, KeyException; + PayloadSupplier decrypt(SymmetricAeadDecryptionRequest request) throws SecurityException; } diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadDecryptionRequest.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadDecryptionRequest.java index 70e4bbfa..1f859d7f 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadDecryptionRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadDecryptionRequest.java @@ -3,5 +3,5 @@ package io.jsonwebtoken.security; /** * @since JJWT_RELEASE_VERSION */ -public interface SymmetricAeadDecryptionRequest extends SymmetricAeadRequest, InitializationVectorSource, AuthenticationTagSource { +public interface SymmetricAeadDecryptionRequest extends SymmetricAeadRequest, InitializationVectorSupplier, DigestSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadRequest.java b/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadRequest.java index c261c6f4..3cb1521d 100644 --- a/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/SymmetricAeadRequest.java @@ -5,5 +5,5 @@ import javax.crypto.SecretKey; /** * @since JJWT_RELEASE_VERSION */ -public interface SymmetricAeadRequest extends CryptoRequest, AssociatedDataSource { +public interface SymmetricAeadRequest extends CryptoRequest, AssociatedDataSupplier { } diff --git a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java index a03a2bcd..73c6d5fe 100644 --- a/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java +++ b/api/src/main/java/io/jsonwebtoken/security/VerifySignatureRequest.java @@ -5,7 +5,5 @@ import java.security.Key; /** * @since JJWT_RELEASE_VERSION */ -public interface VerifySignatureRequest extends SignatureRequest { - - byte[] getSignature(); +public interface VerifySignatureRequest extends SignatureRequest, DigestSupplier { } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java index efbd840e..2c01c708 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweBuilder.java @@ -7,7 +7,6 @@ import io.jsonwebtoken.impl.lang.Function; import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.security.DefaultKeyRequest; -import io.jsonwebtoken.impl.security.DefaultPBEKey; import io.jsonwebtoken.impl.security.DefaultSymmetricAeadRequest; import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.Serializer; @@ -16,25 +15,24 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.AeadResult; -import io.jsonwebtoken.security.EncryptedKeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithms; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.PbeKey; import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import io.jsonwebtoken.security.SymmetricAeadRequest; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.interfaces.PBEKey; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Map; public class DefaultJweBuilder extends DefaultJwtBuilder implements JweBuilder { - private static final SecretKey EMPTY_SECRET_KEY = new SecretKeySpec("NONE".getBytes(StandardCharsets.UTF_8), "NONE"); - private SymmetricAeadAlgorithm enc; // MUST be Symmetric AEAD per https://tools.ietf.org/html/rfc7516#section-4.1.2 private Function encFunction; @@ -82,6 +80,12 @@ public class DefaultJweBuilder extends DefaultJwtBuilder implements @Override public JweBuilder withKey(SecretKey key) { + if (key instanceof PBEKey) { + key = Keys.toPbeKey((PBEKey) key); + } + if (key instanceof PbeKey) { + return withKeyFrom((PbeKey) key, KeyAlgorithms.PBES2_HS512_A256KW); + } return withKeyFrom(key, KeyAlgorithms.DIRECT); } @@ -104,11 +108,6 @@ public class DefaultJweBuilder extends DefaultJwtBuilder implements return this; } - @Override - public JweBuilder withKeyFrom(char[] password, int iterations, EncryptedKeyAlgorithm alg) { - return withKeyFrom(new DefaultPBEKey(password, iterations, alg.getId()), alg); - } - @Override public String compact() { @@ -146,15 +145,13 @@ public class DefaultJweBuilder extends DefaultJwtBuilder implements jweHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); } - SecretKey cek = alg instanceof EncryptedKeyAlgorithm ? enc.generateKey() : EMPTY_SECRET_KEY; //for algorithms that don't need one - KeyRequest keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, cek, this.key, jweHeader); + KeyRequest keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, null, this.key, jweHeader, enc); KeyResult keyResult = algFunction.apply(keyRequest); Assert.state(keyResult != null, "KeyAlgorithm must return a KeyResult."); - cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key."); + SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key."); byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty."); - jweHeader.putAll(keyResult.getHeaderParams()); jweHeader.setAlgorithm(alg.getId()); jweHeader.setEncryptionAlgorithm(enc.getId()); @@ -167,7 +164,7 @@ public class DefaultJweBuilder extends DefaultJwtBuilder implements 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.getAuthenticationTag(), "Encryption result must have a non-empty authentication tag."); + 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); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 61addcf7..a291d193 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -122,13 +122,13 @@ public class DefaultJwtBuilder> implements JwtBuilder } @Override - public T setHeader(Map header) { + public T setHeader(Map header) { this.header = new DefaultHeader<>(header); return (T)this; } @Override - public T setHeaderParams(Map params) { + public T setHeaderParams(Map params) { if (!Collections.isEmpty(params)) { Header header = ensureHeader(); header.putAll(params); @@ -237,7 +237,7 @@ public class DefaultJwtBuilder> implements JwtBuilder } @Override - public T addClaims(Map claims) { + public T addClaims(Map claims) { ensureClaims().putAll(claims); return (T)this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 2557f35c..615c23c1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -455,7 +455,7 @@ public class DefaultJwtParser implements JwtParser { throw new UnsupportedJwtException(msg); } - KeyRequest request = new DefaultKeyRequest<>(this.provider, null, cekBytes, key, jweHeader); + KeyRequest request = new DefaultKeyRequest<>(this.provider, null, cekBytes, key, jweHeader, encAlg); final SecretKey cek = keyAlg.getDecryptionKey(request); SymmetricAeadDecryptionRequest decryptRequest = diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 9c9108d9..3b814a6d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -216,7 +216,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder { @Override public JwtParserBuilder addEncryptionAlgorithms(Collection encAlgs) { - Assert.notEmpty(encAlgs, "Additional EncryptionAlgorithm collection cannot be null or empty."); + Assert.notEmpty(encAlgs, "Additional SymmetricAeadAlgorithm collection cannot be null or empty."); this.extraEncryptionAlgorithms.addAll(encAlgs); return this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java index 26809afa..cbc1524d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -66,7 +66,7 @@ public final class Bytes { return ints; } - public static byte[] plus(byte[]... arrays) { + public static byte[] concat(byte[]... arrays) { int len = 0; int count = Arrays.length(arrays); for(int i = 0; i < count; i++) { @@ -90,6 +90,14 @@ public final class Bytes { return bytes == null ? 0 : bytes.length * (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) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java deleted file mode 100644 index b26eb026..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Supplier.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jsonwebtoken.impl.lang; - -public interface Supplier { - - T get(); -} 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..36075d7a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/ValueGetter.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl.lang; + +import java.math.BigInteger; + +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); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java index 1ec5b068..1825b5de 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractEcJwkFactory.java @@ -96,8 +96,9 @@ abstract class AbstractEcJwkFactory> ext } /** - * Returns {@code true} if a given 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: + * 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 *

@@ -125,24 +126,6 @@ abstract class AbstractEcJwkFactory> ext return lhs.equals(rhs); } - 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 (ECPublicKey) kf.generatePublic(spec); - } catch (Exception e) { - String msg = "Unable to derive ECPublicKey from ECPrivateKey {" + ctx + "}."; - throw new UnsupportedKeyException(msg); - } - } - }); - } - /** * Multiply a point {@code p} by scalar {@code s} on the curve identified by {@code spec}. * @@ -217,4 +200,22 @@ abstract class AbstractEcJwkFactory> ext AbstractEcJwkFactory(Class keyType) { super(DefaultEcPublicJwk.TYPE_VALUE, keyType); } + + 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 (ECPublicKey) kf.generatePublic(spec); + } catch (Exception e) { + String msg = "Unable to derive ECPublicKey from ECPrivateKey {" + ctx + "}."; + throw new UnsupportedKeyException(msg); + } + } + }); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java index 9bcd276f..9fe607c9 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractFamilyJwkFactory.java @@ -1,13 +1,10 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; -import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.MalformedKeyException; import java.math.BigInteger; import java.security.Key; @@ -17,35 +14,6 @@ import java.security.spec.InvalidKeySpecException; abstract class AbstractFamilyJwkFactory> implements FamilyJwkFactory { - static void malformed(String msg) { - throw new MalformedKeyException(msg); - } - - static String getRequiredString(JwkContext ctx, String name) { - Assert.notNull(ctx, "JWK map cannot be null or empty."); - Object value = ctx.get(name); - if (value == null) { - malformed("JWK is missing required case-sensitive '" + name + "' member."); - } - String s = String.valueOf(value); - if (!Strings.hasText(s)) { - malformed("JWK '" + name + "' member cannot be null or empty."); - } - return s; - } - - static BigInteger getRequiredBigInt(JwkContext ctx, String name, boolean sensitive) { - String s = getRequiredString(ctx, name); - try { - byte[] bytes = Decoders.BASE64URL.decode(s); - return new BigInteger(1, bytes); - } catch (Exception e) { - String val = sensitive ? AbstractJwk.REDACTED_VALUE : s; - String msg = "Unable to decode JWK member '" + name + "' to BigInteger from value: " + val; - throw new MalformedKeyException(msg, e); - } - } - // Copied from Apache Commons Codec 1.14: // https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775 static byte[] toUnsignedBytes(BigInteger bigInt) { @@ -109,9 +77,9 @@ abstract class AbstractFamilyJwkFactory> impleme } protected T generateKey(final JwkContext ctx, final Class type, final CheckedFunction fn) { - return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new InstanceCallback() { + return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new CheckedFunction() { @Override - public T doWithInstance(KeyFactory instance) throws Exception { + public T apply(KeyFactory instance) throws Exception { try { return fn.apply(instance); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java index 6ec5cd7a..ffb9290a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -62,10 +62,7 @@ abstract class AbstractJwk implements Jwk { @Override public boolean containsKey(Object key) { - if (key instanceof String) { - return this.context.containsKey((String) key); - } - return false; + return this.context.containsKey(key); } @Override @@ -75,10 +72,7 @@ abstract class AbstractJwk implements Jwk { @Override public Object get(Object key) { - if (key instanceof String) { - return this.context.get((String) key); - } - return null; + return this.context.get(key); } @Override @@ -132,9 +126,8 @@ abstract class AbstractJwk implements Jwk { @Override public boolean equals(Object obj) { - if (obj instanceof AbstractJwk) { - AbstractJwk other = (AbstractJwk) obj; - return this.context.equals(other.context); + if (obj instanceof Map) { + return this.context.equals(obj); } return false; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityRequest.java new file mode 100644 index 00000000..0540c743 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityRequest.java @@ -0,0 +1,27 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.SecurityRequest; + +import java.security.Provider; +import java.security.SecureRandom; + +abstract class AbstractSecurityRequest implements SecurityRequest { + + private final Provider provider; + private final SecureRandom secureRandom; + + public AbstractSecurityRequest(Provider provider, SecureRandom secureRandom) { + 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/AbstractSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java index ead2bfc0..baeb1e27 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSignatureAlgorithm.java @@ -45,7 +45,7 @@ abstract class AbstractSignatureAlgorithm extend public boolean verify(VerifySignatureRequest request) throws SecurityException { final VK key = Assert.notNull(request.getKey(), "Request key cannot be null."); Assert.notEmpty(request.getPayload(), "Request payload cannot be null or empty."); - Assert.notEmpty(request.getSignature(), "Request signature byte array 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); @@ -59,7 +59,7 @@ abstract class AbstractSignatureAlgorithm extend } protected boolean doVerify(VerifySignatureRequest request) throws Exception { - byte[] providedSignature = request.getSignature(); + byte[] providedSignature = request.getDigest(); Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty."); @SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest)request); return MessageDigest.isEqual(providedSignature, computedSignature); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java index df378a1c..c2d52bbd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesAlgorithm.java @@ -1,12 +1,13 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.Bytes; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.AssociatedDataSource; -import io.jsonwebtoken.security.CryptoRequest; -import io.jsonwebtoken.security.InitializationVectorSource; -import io.jsonwebtoken.security.SecurityRequest; +import io.jsonwebtoken.security.AssociatedDataSupplier; +import io.jsonwebtoken.security.InitializationVectorSupplier; +import io.jsonwebtoken.security.KeySupplier; import io.jsonwebtoken.security.SecretKeyGenerator; +import io.jsonwebtoken.security.SecurityRequest; import io.jsonwebtoken.security.WeakKeyException; import javax.crypto.SecretKey; @@ -29,41 +30,41 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato "requests that do not include initialization vectors. AES ciphertext without an IV is weak and " + "susceptible to attack."; - protected final int keyLength; - protected final int ivLength; - protected final int tagLength; + protected final int keyBitLength; + protected final int ivBitLength; + protected final int tagBitLength; protected final boolean gcm; - AesAlgorithm(String id, String jcaTransformation, int keyLength) { + AesAlgorithm(String id, String jcaTransformation, int keyBitLength) { super(id, jcaTransformation); - Assert.isTrue(keyLength == 128 || keyLength == 192 || keyLength == 256, "Invalid AES key length: it must equal 128, 192, or 256."); - this.keyLength = keyLength; + 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.ivLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE); - // https://tools.ietf.org/html/rfc7518#section-5.2.3 through ttps://tools.ietf.org/html/rfc7518#section-5.3 : - this.tagLength = this.gcm ? BLOCK_SIZE : this.keyLength; + 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; } @Override public SecretKey generateKey() { - return new JcaTemplate(KEY_ALG_NAME, null).generateSecretKey(this.keyLength); + return new JcaTemplate(KEY_ALG_NAME, null).generateSecretKey(this.keyBitLength); //TODO: assert generated key length? } - protected SecretKey assertKey(CryptoRequest request) { + protected SecretKey assertKey(KeySupplier request) { SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); validateLengthIfPossible(key); return key; } private void validateLengthIfPossible(SecretKey key) { - validateLength(key, this.keyLength, false); + validateLength(key, this.keyBitLength, false); } protected static String lengthMsg(String id, String type, int requiredLengthInBits, int actualLengthInBits) { - return "The '" + id + "' algorithm requires " + type + " with a length of " + requiredLengthInBits + - " bits (" + (requiredLengthInBits / Byte.SIZE) + " bytes). The provided key has a length of " + - actualLengthInBits + " bits (" + actualLengthInBits / Byte.SIZE + " bytes)."; + 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) { @@ -88,8 +89,8 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato byte[] assertIvLength(final byte[] iv) { int length = length(iv); - if ((this.ivLength / Byte.SIZE) != length) { - String msg = lengthMsg(getId(), "initialization vectors", this.ivLength, length * Byte.SIZE); + if ((this.ivBitLength / Byte.SIZE) != length) { + String msg = lengthMsg(getId(), "initialization vectors", this.ivBitLength, length * Byte.SIZE); throw new IllegalArgumentException(msg); } return iv; @@ -97,14 +98,14 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato byte[] assertTag(byte[] tag) { int len = Arrays.length(tag) * Byte.SIZE; - if (this.tagLength != len) { - String msg = lengthMsg(getId(), "authentication tags", this.tagLength, len); + if (this.tagBitLength != len) { + String msg = lengthMsg(getId(), "authentication tags", this.tagBitLength, len); throw new IllegalArgumentException(msg); } return tag; } - byte[] assertDecryptionIv(InitializationVectorSource src) throws IllegalArgumentException { + byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgumentException { byte[] iv = src.getInitializationVector(); Assert.notEmpty(iv, DECRYPT_NO_IV); return assertIvLength(iv); @@ -112,10 +113,10 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato protected byte[] ensureInitializationVector(SecurityRequest request) { byte[] iv = null; - if (request instanceof InitializationVectorSource) { - iv = Arrays.clean(((InitializationVectorSource) request).getInitializationVector()); + if (request instanceof InitializationVectorSupplier) { + iv = Arrays.clean(((InitializationVectorSupplier) request).getInitializationVector()); } - int ivByteLength = this.ivLength / Byte.SIZE; + int ivByteLength = this.ivBitLength / Byte.SIZE; if (iv == null || iv.length == 0) { iv = new byte[ivByteLength]; SecureRandom random = ensureSecureRandom(request); @@ -135,16 +136,9 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato protected byte[] getAAD(SecurityRequest request) { byte[] aad = null; - if (request instanceof AssociatedDataSource) { - aad = Arrays.clean(((AssociatedDataSource) request).getAssociatedData()); + if (request instanceof AssociatedDataSupplier) { + aad = Arrays.clean(((AssociatedDataSupplier) request).getAssociatedData()); } return aad; } - - protected byte[] plus(byte[] a, byte[] b) { - byte[] c = new byte[length(a) + length(b)]; - System.arraycopy(a, 0, c, 0, a.length); - System.arraycopy(b, 0, c, a.length, b.length); - return c; - } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java index 68501246..f80ae47b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithm.java @@ -1,27 +1,26 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Maps; -import io.jsonwebtoken.security.EncryptedKeyAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import javax.crypto.Cipher; import javax.crypto.SecretKey; import java.security.Key; import java.security.spec.AlgorithmParameterSpec; -import java.util.Map; /** * @since JJWT_RELEASE_VERSION */ -public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgorithm { +public class AesGcmKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm { public static final String TRANSFORMATION = "AES/GCM/NoPadding"; @@ -34,19 +33,20 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo Assert.notNull(request, "request cannot be null."); final SecretKey kek = assertKey(request); - final SecretKey cek = Assert.notNull(request.getPayload(), "Request content encryption key (request.getPayload()) cannot be null."); + SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); final byte[] iv = ensureInitializationVector(request); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); - byte[] taggedCiphertext = execute(request, Cipher.class, new InstanceCallback() { + byte[] taggedCiphertext = execute(request, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.WRAP_MODE, kek, ivSpec); return cipher.wrap(cek); } }); - int tagByteLength = this.tagLength / Byte.SIZE; + 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]; @@ -56,9 +56,10 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo String encodedIv = Encoders.BASE64URL.encode(iv); String encodedTag = Encoders.BASE64URL.encode(tag); - Map extraParams = Maps.of("iv", encodedIv).and("tag", encodedTag).build(); + request.getHeader().put("iv", encodedIv); + request.getHeader().put("tag", encodedTag); - return new DefaultKeyResult(ciphertext, cek, extraParams); + return new DefaultKeyResult(cek, ciphertext); } @Override @@ -67,53 +68,22 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo final SecretKey kek = assertKey(request); final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); final JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); - final byte[] tag = getHeaderByteArray(header, "tag", this.tagLength / Byte.SIZE); - final byte[] iv = getHeaderByteArray(header, "iv", this.ivLength / Byte.SIZE); + final ValueGetter getter = new DefaultValueGetter(header); + final byte[] tag = getter.getRequiredBytes("tag", this.tagBitLength / Byte.SIZE); + final byte[] iv = getter.getRequiredBytes("iv", this.ivBitLength / Byte.SIZE); 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 = plus(cekBytes, tag); + final byte[] taggedCiphertext = Bytes.concat(cekBytes, tag); - return execute(request, Cipher.class, new InstanceCallback() { + return execute(request, Cipher.class, new CheckedFunction() { @Override - public SecretKey doWithInstance(Cipher cipher) throws Exception { + 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; + return (SecretKey) key; } }); } - - private byte[] getHeaderByteArray(JweHeader header, String name, int requiredByteLength) { - Object value = header.get(name); - if (value == null) { - String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value."; - throw new MalformedJwtException(msg); - } - if (!(value instanceof String)) { - String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be a Base64URL-encoded String. Actual type: " + value.getClass().getName(); - throw new MalformedJwtException(msg); - } - String encoded = (String)value; - - byte[] decoded; - try { - decoded = Decoders.BASE64URL.decode(encoded); - } catch (Exception e) { - String msg = "JweHeader '" + name + "' value '" + encoded + - "' does not appear to be a valid Base64URL String: " + e.getMessage(); - throw new MalformedJwtException(msg, e); - } - - int len = Arrays.length(decoded); - if (len != requiredByteLength) { - String msg = "The '" + getId() + "' key management algorithm requires the JweHeader '" + name + - "' value to be " + (requiredByteLength * Byte.SIZE) + " bits (" + requiredByteLength + - " bytes) in length. Actual length: " + (len * Byte.SIZE) + " bits (" + len + " bytes)."; - throw new MalformedJwtException(msg); - } - - return decoded; - } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java index 950d1875..4f200295 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AesWrapKeyAlgorithm.java @@ -1,10 +1,12 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptedKeyAlgorithm; +import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -13,7 +15,7 @@ import java.security.Key; /** * @since JJWT_RELEASE_VERSION */ -public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgorithm { +public class AesWrapKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm { private static final String TRANSFORMATION = "AESWrap"; @@ -25,17 +27,19 @@ public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlg public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); final SecretKey kek = assertKey(request); - final SecretKey cek = Assert.notNull(request.getPayload(), "Request content encryption key (request.getPayload()) cannot be null."); + SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = enc.generateKey(); + Assert.notNull(cek, "Request encryption algorithm cannot generate a null key."); - byte[] ciphertext = execute(request, Cipher.class, new InstanceCallback() { + byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.WRAP_MODE, kek); return cipher.wrap(cek); } }); - return new DefaultKeyResult(ciphertext, cek); + return new DefaultKeyResult(cek, ciphertext); } @Override @@ -44,9 +48,9 @@ public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlg final SecretKey kek = assertKey(request); final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); - return execute(request, Cipher.class, new InstanceCallback() { + return execute(request, Cipher.class, new CheckedFunction() { @Override - public SecretKey doWithInstance(Cipher cipher) throws Exception { + 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."); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java index 965a02d5..49b12cc9 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ConcatKDF.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.CryptoException; @@ -29,9 +30,9 @@ final class ConcatKDF extends CryptoAlgorithm { ConcatKDF(String jcaName) { super("ConcatKDF", jcaName); - int hashByteLength = new JcaTemplate(jcaName, null).execute(MessageDigest.class, new InstanceCallback() { + int hashByteLength = execute(MessageDigest.class, new CheckedFunction() { @Override - public Integer doWithInstance(MessageDigest instance) { + public Integer apply(MessageDigest instance) { return instance.getDigestLength(); } }); @@ -74,9 +75,8 @@ final class ConcatKDF extends CryptoAlgorithm { } // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf, Section 5.8.1.1, Input requirement #2: if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) { - String msg = "derivedKeyBitLength for " + getJcaName() + " derived keys may not exceed " + - MAX_DERIVED_KEY_BIT_LENGTH + " bits (" + MAX_DERIVED_KEY_BIT_LENGTH / Byte.SIZE + " bytes). " + - "Specified size: " + derivedKeyBitLength + " bits (" + derivedKeyBitLength / Byte.SIZE + " bytes)."; + String msg = "derivedKeyBitLength for " + getJcaName() + "-derived keys may not exceed " + + bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + ". Specified size: " + bitsMsg(derivedKeyBitLength) + "."; throw new IllegalArgumentException(msg); } @@ -94,11 +94,11 @@ final class ConcatKDF extends CryptoAlgorithm { long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo); assert inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH : "Hash input is too large."; - byte[] derivedKeyBytes = new JcaTemplate(getJcaName(), null).execute(MessageDigest.class, new InstanceCallback() { + byte[] derivedKeyBytes = new JcaTemplate(getJcaName(), null).execute(MessageDigest.class, new CheckedFunction() { @Override - public byte[] doWithInstance(MessageDigest md) throws Exception { + public byte[] apply(MessageDigest md) throws Exception { - final ByteArrayOutputStream stream = new ByteArrayOutputStream((int)derivedKeyByteLength); + final ByteArrayOutputStream stream = new ByteArrayOutputStream((int) derivedKeyByteLength); long kLastIndex = reps - 1; // Section 5.8.1.1, Process step #5: @@ -118,7 +118,7 @@ final class ConcatKDF extends CryptoAlgorithm { // Section 5.8.1.1, Process step #6: if (i == kLastIndex && repsd != (double) reps) { //repsd calculation above didn't result in a whole number: long leftmostBitLength = derivedKeyBitLength % hashBitLength; - int leftmostByteLength = (int)(leftmostBitLength / Byte.SIZE); + int leftmostByteLength = (int) (leftmostBitLength / Byte.SIZE); byte[] kLast = new byte[leftmostByteLength]; System.arraycopy(Ki, 0, kLast, 0, kLast.length); Ki = kLast; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index b73e9872..50ffe673 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.Identifiable; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.SecurityRequest; @@ -30,17 +31,20 @@ abstract class CryptoAlgorithm implements Identifiable { } SecureRandom ensureSecureRandom(SecurityRequest request) { - Assert.notNull(request, "request cannot be null."); - SecureRandom random = request.getSecureRandom(); + SecureRandom random = request != null ? request.getSecureRandom() : null; return random != null ? random : Randoms.secureRandom(); } - protected T execute(SecurityRequest request, Class clazz, InstanceCallback callback) { + protected R execute(Class clazz, CheckedFunction fn) { + return new JcaTemplate(getJcaName(), null).execute(clazz, fn); + } + + protected T execute(SecurityRequest request, Class clazz, CheckedFunction fn) { Assert.notNull(request, "request cannot be null."); Provider provider = request.getProvider(); SecureRandom random = ensureSecureRandom(request); JcaTemplate template = new JcaTemplate(getJcaName(), provider, random); - return template.execute(clazz, callback); + return template.execute(clazz, fn); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java index 05ad3719..531df9ee 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultAeadResult.java @@ -1,8 +1,8 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest; import io.jsonwebtoken.security.AeadResult; +import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest; import javax.crypto.SecretKey; import java.security.Provider; @@ -19,7 +19,7 @@ public class DefaultAeadResult extends DefaultSymmetricAeadRequest implements Ae } @Override - public byte[] getAuthenticationTag() { + public byte[] getDigest() { return this.TAG; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java index 69c43a87..f7e5f20f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultCryptoRequest.java @@ -13,8 +13,8 @@ public class DefaultCryptoRequest extends DefaultPayloadSuppli private final SecureRandom secureRandom; private final K key; - public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, T data, K key) { - super(data); + public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, T payload, K key) { + super(payload); this.provider = provider; this.secureRandom = secureRandom; this.key = Assert.notNull(key, "key cannot be null."); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java index d1955c64..ee7e61b5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEllipticCurveSignatureAlgorithm.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm; import io.jsonwebtoken.security.InvalidKeyException; @@ -55,9 +56,9 @@ public class DefaultEllipticCurveSignatureAlgorithm() { + return template.execute(KeyPairGenerator.class, new CheckedFunction() { @Override - public KeyPair doWithInstance(KeyPairGenerator generator) throws Exception { + public KeyPair apply(KeyPairGenerator generator) throws Exception { generator.initialize(spec, Randoms.secureRandom()); return generator.generateKeyPair(); } @@ -104,9 +105,9 @@ public class DefaultEllipticCurveSignatureAlgorithm request) { - return execute(request, Signature.class, new InstanceCallback() { + return execute(request, Signature.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Signature sig) throws Exception { + public byte[] apply(Signature sig) throws Exception { sig.initSign(request.getKey()); sig.update(request.getPayload()); byte[] signature = sig.sign(); @@ -117,10 +118,10 @@ public class DefaultEllipticCurveSignatureAlgorithm request) { - return execute(request, Signature.class, new InstanceCallback() { + return execute(request, Signature.class, new CheckedFunction() { @Override - public Boolean doWithInstance(Signature sig) throws Exception { - byte[] signature = request.getSignature(); + public Boolean apply(Signature sig) throws Exception { + byte[] signature = request.getDigest(); /* * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java deleted file mode 100644 index 26b7f97b..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJweFactory.java +++ /dev/null @@ -1,123 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.impl.lang.Services; -import io.jsonwebtoken.io.Decoder; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptionAlgorithms; -import io.jsonwebtoken.security.SymmetricAeadAlgorithm; - -import java.util.Map; - -/** - * @since JJWT_RELEASE_VERSION - */ -public class DefaultJweFactory { - - /*private final Decoder base64UrlDecoder; - - private final Deserializer> deserializer; - - private final SymmetricAeadAlgorithm encryptionAlgorithm; - - private static Deserializer> loadDeserializer() { - Deserializer deserializer = Services.loadFirst(Deserializer.class); - //noinspection unchecked - return deserializer; - } - - public DefaultJweFactory() { - //this(Decoders.BASE64URL, loadDeserializer(), EncryptionAlgorithms.A256GCM); - } - - public DefaultJweFactory(Decoder base64UrlDecoder, - Deserializer> deserializer, - EncryptionAlgorithm encryptionAlgorithm) { - this.base64UrlDecoder = Assert.notNull(base64UrlDecoder, "Base64Url TextCodec cannot be null."); - this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); - this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "EncryptionAlgorithm cannot be null."); - } - - /* - - public Jwe createJwe(String base64UrlProtectedHeader, String base64UrlEncryptedKey, String base64UrlIv, - String base64UrlCiphertext, String base64UrlAuthenticationTag) { - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #2 - // ==================================================================== - - final byte[] headerBytes = base64UrlDecode(base64UrlProtectedHeader, "Protected Header"); - - // encrypted key can be null with Direct Key or Direct Key Agreement - // https://tools.ietf.org/html/rfc7516#section-5.2 - // so we use a 'null safe' variant: - final byte[] encryptedKeyBytes = nullSafeBase64UrlDecode(base64UrlEncryptedKey, "Encrypted Key"); - - final byte[] iv = base64UrlDecode(base64UrlIv, "Initialization Vector"); - - final byte[] ciphertext = base64UrlDecode(base64UrlCiphertext, "Ciphertext"); - - final byte[] authcTag = base64UrlDecode(base64UrlAuthenticationTag, "Authentication Tag"); - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #3 - // ==================================================================== - - Map protectedHeader; - try { - protectedHeader = parseJson(headerBytes); - } catch (Exception e) { - String msg = "JWE Protected Header must be a valid JSON object."; - throw new IllegalArgumentException(msg, e); - } - Assert.notEmpty(protectedHeader, "JWE Protected Header cannot be a null or empty JSON object."); - - DefaultJweHeader header = new DefaultJweHeader(protectedHeader); - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #4 - // ==================================================================== - - // we currently don't support JSON serialization (just compact), so we can skip #4 - - // ==================================================================== - // https://tools.ietf.org/html/rfc7516#section-5.2 #11 and #12 - // ==================================================================== - - - throw new UnsupportedOperationException("Not yet finished."); - - } - - protected byte[] nullSafeBase64UrlDecode(String base64UrlEncoded, String jweName) { - if (base64UrlEncoded == null) { - return null; - } - return base64UrlDecode(base64UrlEncoded, jweName); - } - - protected byte[] base64UrlDecode(String base64UrlEncoded, String jweName) { - - if (base64UrlEncoded == null) { - String msg = "Invalid compact JWE: base64url JWE " + jweName + " is missing."; - throw new IllegalArgumentException(msg); - } - - try { - return base64UrlDecoder.decode(base64UrlEncoded); - } catch (Exception e) { - String msg = "Invalid compact JWE: JWE " + jweName + - " fragment is invalid and cannot be Base64Url-decoded: " + base64UrlEncoded; - throw new IllegalArgumentException(msg, e); - } - } - - @SuppressWarnings("unchecked") - protected Map parseJson(byte[] json) { - return deserializer.deserialize(json); - } - - */ -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index 06797ba3..f29b0409 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -66,10 +66,10 @@ public class DefaultJwkContext implements JwkContext { SETTERS = java.util.Collections.unmodifiableMap(s); } - private final Map values; - private final Map canonicalValues; - private final Map redactedValues; - private final Set privateMemberNames; + private final Map values; // canonical values formatted per RFC requirements + private final Map idiomaticValues; // the values map with any string/encoded values converted to Java type-safe values where possible + private final Map redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation. + private final Set privateMemberNames; // names of values that should be redacted for toString output private K key; private PublicKey publicKey; private Provider provider; @@ -84,7 +84,7 @@ public class DefaultJwkContext implements JwkContext { public DefaultJwkContext(Set privateMemberNames) { this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty."); this.values = new LinkedHashMap<>(); - this.canonicalValues = new LinkedHashMap<>(); + this.idiomaticValues = new LinkedHashMap<>(); this.redactedValues = new LinkedHashMap<>(); } @@ -111,7 +111,7 @@ public class DefaultJwkContext implements JwkContext { DefaultJwkContext src = (DefaultJwkContext) other; this.provider = other.getProvider(); this.values = new LinkedHashMap<>(src.values); - this.canonicalValues = new LinkedHashMap<>(src.canonicalValues); + this.idiomaticValues = new LinkedHashMap<>(src.idiomaticValues); this.redactedValues = new LinkedHashMap<>(src.redactedValues); if (removePrivate) { for (String name : this.privateMemberNames) { @@ -126,7 +126,7 @@ public class DefaultJwkContext implements JwkContext { } else { Object redactedValue = this.privateMemberNames.contains(name) ? AbstractJwk.REDACTED_VALUE : value; this.redactedValues.put(name, redactedValue); - this.canonicalValues.put(name, value); + this.idiomaticValues.put(name, value); return this.values.put(name, value); } } @@ -139,13 +139,15 @@ public class DefaultJwkContext implements JwkContext { } else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) { value = Collections.arrayToList(value); } - return doPut(name, value); + return idiomaticPut(name, value); } - private Object doPut(String name, Object 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 name != null; //asserted by caller. Canonicalizer fn = SETTERS.get(name); - if (fn != null) { //Setting a JWA-standard property - let's ensure we can represent it canonically: + if (fn != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: return fn.apply(this, value); } else { //non-standard/custom property: return nullSafePut(name, value); @@ -153,17 +155,17 @@ public class DefaultJwkContext implements JwkContext { } @Override - public JwkContext putAll(Map m) { + public void putAll(Map m) { Assert.notEmpty(m, "JWK values cannot be null or empty."); for (Map.Entry entry : m.entrySet()) { put(entry.getKey(), entry.getValue()); } - return this; } - private Object remove(String key) { + @Override + public Object remove(Object key) { this.redactedValues.remove(key); - this.canonicalValues.remove(key); + this.idiomaticValues.remove(key); return this.values.remove(key); } @@ -178,7 +180,7 @@ public class DefaultJwkContext implements JwkContext { } @Override - public boolean containsKey(String key) { + public boolean containsKey(Object key) { return this.values.containsKey(key); } @@ -188,10 +190,15 @@ public class DefaultJwkContext implements JwkContext { } @Override - public Object get(String key) { + public Object get(Object key) { return this.values.get(key); } + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear JwkContext objects."); + } + @Override public Set keySet() { return this.values.keySet(); @@ -207,14 +214,9 @@ public class DefaultJwkContext implements JwkContext { return this.values.entrySet(); } - @Override - public Map getValues() { - return this.values; - } - @Override public String getAlgorithm() { - return (String) this.canonicalValues.get(AbstractJwk.ALGORITHM); + return (String) this.values.get(AbstractJwk.ALGORITHM); } @Override @@ -225,7 +227,7 @@ public class DefaultJwkContext implements JwkContext { @Override public String getId() { - return (String) this.canonicalValues.get(AbstractJwk.ID); + return (String) this.values.get(AbstractJwk.ID); } @Override @@ -237,7 +239,7 @@ public class DefaultJwkContext implements JwkContext { @Override public Set getOperations() { //noinspection unchecked - return (Set) this.canonicalValues.get(AbstractJwk.OPERATIONS); + return (Set) this.idiomaticValues.get(AbstractJwk.OPERATIONS); } @Override @@ -248,7 +250,7 @@ public class DefaultJwkContext implements JwkContext { @Override public String getType() { - return (String) this.canonicalValues.get(AbstractJwk.TYPE); + return (String) this.values.get(AbstractJwk.TYPE); } @Override @@ -259,7 +261,7 @@ public class DefaultJwkContext implements JwkContext { @Override public String getPublicKeyUse() { - return (String) this.canonicalValues.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE); + return (String) this.values.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE); } @Override @@ -271,7 +273,7 @@ public class DefaultJwkContext implements JwkContext { @Override public List getX509CertificateChain() { //noinspection unchecked - return (List) this.canonicalValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN); + return (List) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN); } @Override @@ -282,7 +284,7 @@ public class DefaultJwkContext implements JwkContext { @Override public byte[] getX509CertificateSha1Thumbprint() { - return (byte[]) this.canonicalValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT); + return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT); } @Override @@ -293,7 +295,7 @@ public class DefaultJwkContext implements JwkContext { @Override public byte[] getX509CertificateSha256Thumbprint() { - return (byte[]) this.canonicalValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT); + return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT); } @Override @@ -304,7 +306,7 @@ public class DefaultJwkContext implements JwkContext { @Override public URI getX509Url() { - return (URI) this.canonicalValues.get(AbstractAsymmetricJwk.X509_URL); + return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_URL); } @Override @@ -353,18 +355,13 @@ public class DefaultJwkContext implements JwkContext { @Override public int hashCode() { - int hash = 7; - hash = hash * 31 + Objects.nullSafeHashCode(this.key); - hash = hash * 31 + Objects.nullSafeHashCode(this.values); - return hash; + return this.values.hashCode(); } @Override public boolean equals(Object obj) { - if (obj instanceof DefaultJwkContext) { - DefaultJwkContext c = (DefaultJwkContext) obj; - return Objects.nullSafeEquals(this.key, c.key) && - Objects.nullSafeEquals(this.values, c.values); + if (obj instanceof Map) { + return this.values.equals(obj); } return false; } @@ -403,23 +400,23 @@ public class DefaultJwkContext implements JwkContext { public T apply(DefaultJwkContext ctx, Object rawValue) { if (JwtMap.isReduceableToNull(rawValue)) { - //noinspection unchecked - return (T) ctx.remove(id); + ctx.remove(id); + return null; } - T canonicalValue; - Object encodedValue; + T idiomaticValue; // preferred Java format + Object canonicalValue; //as required by the RFC try { - canonicalValue = converter.applyFrom(rawValue); - encodedValue = converter.applyTo(canonicalValue); + idiomaticValue = converter.applyFrom(rawValue); + canonicalValue = converter.applyTo(idiomaticValue); } catch (Exception e) { - String msg = "Invalid JWK " + title + "('" + id + "') value [" + rawValue + "]: " + e.getMessage(); + String msg = "Invalid JWK '" + id + "' (" + title + ") value [" + rawValue + "]: " + e.getMessage(); throw new MalformedKeyException(msg, e); } - ctx.nullSafePut(id, encodedValue); - ctx.canonicalValues.put(id, canonicalValue); + ctx.nullSafePut(id, canonicalValue); + ctx.idiomaticValues.put(id, idiomaticValue); //noinspection unchecked - return (T) encodedValue; + return (T) canonicalValue; } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java index 871facd0..5f551e28 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyRequest.java @@ -3,6 +3,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import java.security.Key; import java.security.Provider; @@ -11,14 +12,29 @@ import java.security.SecureRandom; public class DefaultKeyRequest extends DefaultCryptoRequest implements KeyRequest { private final JweHeader header; + private final SymmetricAeadAlgorithm encryptionAlgorithm; - public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, T data, K key, JweHeader header) { - super(provider, secureRandom, data, key); + public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, T payload, K key, JweHeader header, SymmetricAeadAlgorithm encryptionAlgorithm) { + super(provider, secureRandom, payload, key); this.header = Assert.notNull(header, "JweHeader cannot be null."); + this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "SymmetricAeadAlgorithm argument cannot be null."); + } + + @Override + protected T assertValidPayload(T payload) throws IllegalArgumentException { + if (payload != null) { + return super.assertValidPayload(payload); + } + return null; } @Override public JweHeader getHeader() { return this.header; } + + @Override + public SymmetricAeadAlgorithm 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 index b9a68316..609520e4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyResult.java @@ -1,34 +1,23 @@ 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; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; public class DefaultKeyResult implements KeyResult { - private static final byte[] EMPTY_BYTES = new byte[0]; - private final byte[] payload; private final SecretKey key; - private final Map headerParams; public DefaultKeyResult(SecretKey key) { - this(EMPTY_BYTES, key); + this(key, Bytes.EMPTY); } - public DefaultKeyResult(byte[] encryptedKey, SecretKey key) { - this(encryptedKey, key, Collections.emptyMap()); - } - - public DefaultKeyResult(byte[] encryptedKey, SecretKey key, Map headerParams) { + public DefaultKeyResult(SecretKey key, byte[] encryptedKey) { this.payload = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty)."); this.key = Assert.notNull(key, "Key argument cannot be null."); - Assert.notNull(headerParams, "headerParams cannot be null."); - this.headerParams = Collections.unmodifiableMap(new LinkedHashMap<>(headerParams)); } @Override @@ -40,9 +29,4 @@ public class DefaultKeyResult implements KeyResult { public SecretKey getKey() { return this.key; } - - @Override - public Map getHeaderParams() { - return this.headerParams; - } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPBEKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPBEKey.java deleted file mode 100644 index 7f080db4..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPBEKey.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Objects; - -import javax.crypto.interfaces.PBEKey; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; - -public final class DefaultPBEKey implements PBEKey { - - private static final String RAW_FORMAT = "RAW"; - - private volatile boolean destroyed = false; - private final char[] chars; - private final byte[] bytes; - private final int iterations; - private final String algorithm; - - private static byte[] toBytes(char[] chars) { - ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars)); - byte[] bytes = new byte[buf.limit()]; - buf.get(bytes); - return bytes; - } - - public DefaultPBEKey(char[] password, int iterations, String algorithm) { - boolean empty = Objects.isEmpty(password); - this.chars = empty ? new char[0] : password.clone(); - this.bytes = empty ? new byte[0] : toBytes(this.chars); - if (iterations <= 0) { - String msg = "Iterations must be an integer greater than zero. Value: " + iterations; - throw new IllegalArgumentException(msg); - } - this.iterations = iterations; - this.algorithm = Assert.hasText(algorithm, "Algorithm string cannot be null or empty."); - } - - private void assertActive() { - if (destroyed) { - String msg = "PBKey has been destroyed. Password characters or bytes may not be obtained."; - throw new IllegalStateException(msg); - } - } - - @Override - public char[] getPassword() { - assertActive(); - return this.chars.clone(); - } - - @Override - public byte[] getSalt() { - return null; - } - - @Override - public int getIterationCount() { - return this.iterations; - } - - @Override - public String getAlgorithm() { - return this.algorithm; - } - - @Override - public String getFormat() { - return RAW_FORMAT; - } - - @Override - public byte[] getEncoded() { - assertActive(); - return this.bytes.clone(); - } - - @Override - public void destroy() { - if (destroyed) return; - java.util.Arrays.fill(bytes, (byte) 0); - java.util.Arrays.fill(chars, '\u0000'); - this.destroyed = true; - } - - @Override - public boolean isDestroyed() { - return destroyed; - } -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java index b6c1f82a..efa777ae 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPayloadSupplier.java @@ -10,11 +10,18 @@ class DefaultPayloadSupplier implements PayloadSupplier { private final T payload; DefaultPayloadSupplier(T payload) { - this.payload = Assert.notNull(payload, "payload cannot be null."); - Assert.isTrue(payload instanceof byte[] || payload instanceof Key, "Payload argument must be either a byte array or a java.security.Key."); - if (payload instanceof byte[] && ((byte[]) payload).length == 0) { - throw new IllegalArgumentException("Payload byte array cannot be empty."); + this.payload = assertValidPayload(payload); + } + + protected T assertValidPayload(T payload) throws IllegalArgumentException { + Assert.notNull(payload, "payload cannot be null."); + if (payload instanceof byte[]) { + Assert.notEmpty((byte[])payload, "payload byte array cannot be empty."); + } else if (!(payload instanceof Key)) { + String msg = "payload must be either a byte array or a java.security.Key instance."; + throw new IllegalArgumentException(msg); } + return payload; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java new file mode 100644 index 00000000..aacbe540 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKey.java @@ -0,0 +1,104 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.security.PbeKey; + +public class DefaultPbeKey implements PbeKey { + + private static final String RAW_FORMAT = "RAW"; + private static final String NONE_ALGORITHM = "NONE"; + + private volatile boolean destroyed = false; + private final char[] chars; + //private final byte[] bytes; + private final int workFactor; + +// private static byte[] toBytes(char[] chars) { +// ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars)); +// byte[] bytes = new byte[buf.limit()]; +// buf.get(bytes); +// return bytes; +// } + + public DefaultPbeKey(char[] password, int workFactor) { + boolean empty = Objects.isEmpty(password); + this.chars = empty ? new char[0] : password.clone(); + //this.bytes = empty ? new byte[0] : toBytes(this.chars); + if (workFactor < 0) { + String msg = "workFactor cannot be negative. Value: " + workFactor; + throw new IllegalArgumentException(msg); + } + this.workFactor = workFactor; + } + + private void assertActive() { + if (destroyed) { + String msg = "PBKey has been destroyed. Password characters or bytes may not be obtained."; + throw new IllegalStateException(msg); + } + } + + @Override + public char[] getPassword() { + assertActive(); + return this.chars.clone(); + } + + @Override + public int getWorkFactor() { + return this.workFactor; + } + + @Override + public String getAlgorithm() { + return NONE_ALGORITHM; + } + + @Override + public String getFormat() { + return RAW_FORMAT; + } + + @Override + public byte[] getEncoded() { + throw new UnsupportedOperationException("getEncoded is not supported for PbeKey instances."); + //assertActive(); + //return this.bytes.clone(); + } + + @Override + public void destroy() { +// if (bytes != null) { +// java.util.Arrays.fill(bytes, (byte) 0); +// } + if (chars != null) { + java.util.Arrays.fill(chars, '\u0000'); + } + this.destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public int hashCode() { + return Objects.nullSafeHashCode(this.chars); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DefaultPbeKey) { + DefaultPbeKey other = (DefaultPbeKey) obj; + return this.workFactor == other.workFactor && + Objects.nullSafeEquals(this.chars, other.chars); + } + return false; + } + + @Override + public String toString() { + return "password=, workFactor=" + this.workFactor; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java new file mode 100644 index 00000000..5cff23b7 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultPbeKeyBuilder.java @@ -0,0 +1,96 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PbeKeyBuilder; + +import javax.crypto.interfaces.PBEKey; +import javax.security.auth.Destroyable; + +// +// MAINTAINER NOTE: +// +// If editing/modifying this class, DO NOT attempt to call jcaKey.getPassword(): doing so creates a clone of that +// character array. There is no need to create copies of sensitive data (that we would be responsible for cleaning up) +// since the JcaPbeKey implementation will just delegate to the jcaKey as needed. +// +public class DefaultPbeKeyBuilder implements PbeKeyBuilder, Destroyable { + + private char[] password; + private int workFactor; + private PBEKey jcaKey; + private volatile boolean destroyed; + + private static char[] assertPassword(char[] password) { + Assert.notEmpty(password, "Password cannot be null or empty."); + return password; + } + + private static int assertWorkFactor(int workFactor) { + if (workFactor < 0) { + String msg = "workFactor cannot be negative."; + throw new IllegalArgumentException(msg); + } + return workFactor; + } + + @Override + public PbeKeyBuilder forKey(PBEKey jcaKey) { + this.jcaKey = Assert.notNull(jcaKey, "PBEKey cannot be null."); + return this; + } + + @Override + public PbeKeyBuilder setPassword(String password) { + return setPassword(Assert.notNull(password, "password cannot be null.").toCharArray()); + } + + @Override + public DefaultPbeKeyBuilder setPassword(char[] password) { + this.password = password; + return this; + } + + @Override + public DefaultPbeKeyBuilder setWorkFactor(int workFactor) { + this.workFactor = workFactor; + return this; + } + + @Override + public void destroy() { + if (this.password != null) { + destroyed = true; + java.util.Arrays.fill(this.password, '\u0000'); + } + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + private void assertActive() { + if (destroyed) { + String msg = "This PbeKeyBuilder has been destroyed in order to clean/zero-out internal password " + + "arrays for safety. Please use a new builder for each PbeKey instance you need to create."; + throw new IllegalStateException(msg); + } + } + + @SuppressWarnings("unchecked") + @Override + public K build() { + try { + if (this.jcaKey != null) { + return (K) new JcaPbeKey(this.jcaKey); + } + assertActive(); + assertPassword(this.password); + assertWorkFactor(this.workFactor); + return (K) new DefaultPbeKey(this.password, this.workFactor); + } finally { + destroy(); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java index 7f168dd2..d85e728c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithm.java @@ -1,10 +1,12 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptedKeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.RsaKeyAlgorithm; import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -14,8 +16,8 @@ import java.security.PublicKey; import java.security.interfaces.RSAKey; import java.security.spec.AlgorithmParameterSpec; -public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm - implements EncryptedKeyAlgorithm { +public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm + implements RsaKeyAlgorithm { private final AlgorithmParameterSpec SPEC; //can be null @@ -29,14 +31,15 @@ public class DefaultRsaKeyAlgorithm request) throws SecurityException { + public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException { Assert.notNull(request, "Request cannot be null."); - final EK kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); - final SecretKey cek = Assert.notNull(request.getPayload(), "Request content encryption key (request.getPayload() cannot be null."); + final E kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); + SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); - byte[] ciphertext = execute(request, Cipher.class, new InstanceCallback() { + byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { if (SPEC == null) { cipher.init(Cipher.WRAP_MODE, kek, ensureSecureRandom(request)); } else { @@ -46,18 +49,18 @@ public class DefaultRsaKeyAlgorithm request) throws SecurityException { + public SecretKey getDecryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); - final DK kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null."); + final D kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null."); final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); - return execute(request, Cipher.class, new InstanceCallback() { + return execute(request, Cipher.class, new CheckedFunction() { @Override - public SecretKey doWithInstance(Cipher cipher) throws Exception { + public SecretKey apply(Cipher cipher) throws Exception { if (SPEC == null) { cipher.init(Cipher.UNWRAP_MODE, kek); } else { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java index d15df3e4..b061e4ca 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaSignatureAlgorithm.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.RuntimeEnvironment; import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.RsaSignatureAlgorithm; @@ -102,10 +103,10 @@ public class DefaultRsaSignatureAlgorithm request) throws Exception { - return execute(request, Signature.class, new InstanceCallback() { + protected byte[] doSign(final SignatureRequest request) { + return execute(request, Signature.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Signature sig) throws Exception { + public byte[] apply(Signature sig) throws Exception { if (algorithmParameterSpec != null) { sig.setParameter(algorithmParameterSpec); } @@ -122,15 +123,15 @@ public class DefaultRsaSignatureAlgorithm() { + return execute(request, Signature.class, new CheckedFunction() { @Override - public Boolean doWithInstance(Signature sig) throws Exception { + 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.getSignature()); + return sig.verify(request.getDigest()); } }); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java new file mode 100644 index 00000000..5efaa08d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecurityRequest.java @@ -0,0 +1,27 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.SecurityRequest; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DefaultSecurityRequest implements SecurityRequest { + + private final Provider provider; + private final SecureRandom secureRandom; + + public DefaultSecurityRequest(Provider provider, SecureRandom secureRandom) { + 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/DefaultSymmetricAeadRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricAeadRequest.java index 3982a971..8ef2128a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricAeadRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSymmetricAeadRequest.java @@ -1,13 +1,13 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.security.InitializationVectorSource; +import io.jsonwebtoken.security.InitializationVectorSupplier; import io.jsonwebtoken.security.SymmetricAeadRequest; import javax.crypto.SecretKey; import java.security.Provider; import java.security.SecureRandom; -public class DefaultSymmetricAeadRequest extends DefaultCryptoRequest implements SymmetricAeadRequest, InitializationVectorSource { +public class DefaultSymmetricAeadRequest extends DefaultCryptoRequest implements SymmetricAeadRequest, InitializationVectorSupplier { private final byte[] IV; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java new file mode 100644 index 00000000..c57b3a49 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultValueGetter.java @@ -0,0 +1,149 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JweHeader; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.impl.lang.Bytes; +import io.jsonwebtoken.impl.lang.ValueGetter; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.MalformedKeyException; + +import java.math.BigInteger; +import java.util.Map; + +/** + * Allows use af shared assertions across codebase, regardless of inheritance hierarchy. + */ +public class DefaultValueGetter implements ValueGetter { + + private final Map values; + + public DefaultValueGetter(Map values) { + this.values = Assert.notEmpty(values, "Values cannot be null or empty."); + } + + private String name() { + if (values instanceof JweHeader) { + return "JWE header"; + } else if (values instanceof JwsHeader) { + return "JWS header"; + } else if (values instanceof Header) { + return "JWT header"; + } else if (values instanceof Jwk || values instanceof JwkContext) { + Object value = values.get(AbstractJwk.TYPE); + if (DefaultSecretJwk.TYPE_VALUE.equals(value)) { + value = "Secret"; + } + return value instanceof String ? value + " JWK" : "JWK"; + } else { + return "Map"; + } + } + + private JwtException malformed(String msg) { + if (values instanceof JwkContext || values instanceof Jwk) { + return new MalformedKeyException(msg); + } else { + return new MalformedJwtException(msg); + } + } + + protected Object getRequiredValue(String key) { + Object value = this.values.get(key); + if (value == null) { + String msg = name() + " is missing required '" + key + "' value."; + throw malformed(msg); + } + return value; + } + + @Override + public String getRequiredString(String key) { + Object value = getRequiredValue(key); + if (!(value instanceof String)) { + String msg = name() + " '" + key + "' value must be a String. Actual type: " + value.getClass().getName(); + throw malformed(msg); + } + String sval = Strings.clean((String) value); + if (!Strings.hasText(sval)) { + String msg = name() + " '" + key + "' string value cannot be null or empty."; + throw malformed(msg); + } + return (String) value; + } + + @Override + public int getRequiredInteger(String key) { + Object value = getRequiredValue(key); + if (!(value instanceof Integer)) { + String msg = name() + " '" + key + "' value must be an Integer. Actual type: " + value.getClass().getName(); + throw malformed(msg); + } + return (Integer) value; + } + + @Override + public int getRequiredPositiveInteger(String key) { + int value = getRequiredInteger(key); + if (value <= 0) { + String msg = name() + " '" + key + "' value must be a positive Integer. Value: " + value; + throw malformed(msg); + } + return value; + } + + @Override + public byte[] getRequiredBytes(String key) { + + String encoded = getRequiredString(key); + + byte[] decoded; + try { + decoded = Decoders.BASE64URL.decode(encoded); + } catch (Exception e) { + String msg = name() + " '" + key + "' value is not a valid Base64URL String: " + e.getMessage(); + throw malformed(msg); + } + + if (Arrays.length(decoded) == 0) { + String msg = name() + " '" + key + "' decoded byte array cannot be empty."; + throw malformed(msg); + } + + return decoded; + } + + @Override + public byte[] getRequiredBytes(String key, int requiredByteLength) { + byte[] decoded = getRequiredBytes(key); + int len = Arrays.length(decoded); + if (len != requiredByteLength) { + String msg = name() + " '" + key + "' decoded byte array must be " + Bytes.bytesMsg(requiredByteLength) + + " long. Actual length: " + Bytes.bytesMsg(len) + "."; + throw malformed(msg); + } + return decoded; + } + + @Override + public BigInteger getRequiredBigInt(String key, boolean sensitive) { + String s = getRequiredString(key); + try { + byte[] bytes = Decoders.BASE64URL.decode(s); + return new BigInteger(1, bytes); + } catch (Exception e) { + String msg = "Unable to decode " + name() + " '" + key + "' value"; + if (!sensitive) { + msg += " '" + s + "'"; + } + msg += " to BigInteger: " + e.getMessage(); + throw malformed(msg); + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java index 2c99bfa4..0c7502e5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultVerifySignatureRequest.java @@ -17,7 +17,7 @@ public class DefaultVerifySignatureRequest extends DefaultSignatu } @Override - public byte[] getSignature() { + public byte[] getDigest() { return this.signature; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java index 2219a36a..0c483b07 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPrivateJwkFactory.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.EcPrivateJwk; import io.jsonwebtoken.security.EcPublicJwk; @@ -55,8 +56,9 @@ class EcPrivateJwkFactory extends AbstractEcJwkFactory ctx) { - String curveId = getRequiredString(ctx, DefaultEcPublicJwk.CURVE_ID); - BigInteger d = getRequiredBigInt(ctx, DefaultEcPrivateJwk.D, true); + ValueGetter getter = new DefaultValueGetter(ctx); + String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID); + BigInteger d = getter.getRequiredBigInt(DefaultEcPrivateJwk.D, true); // 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) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java index 6d863f07..a8e3d858 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcPublicJwkFactory.java @@ -1,6 +1,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.InvalidKeyException; @@ -45,9 +46,10 @@ class EcPublicJwkFactory extends AbstractEcJwkFactory @Override protected EcPublicJwk createJwkFromValues(final JwkContext ctx) { - String curveId = getRequiredString(ctx, DefaultEcPublicJwk.CURVE_ID); - BigInteger x = getRequiredBigInt(ctx, DefaultEcPublicJwk.X, false); - BigInteger y = getRequiredBigInt(ctx, DefaultEcPublicJwk.Y, false); + ValueGetter getter = new DefaultValueGetter(ctx); + String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID); + BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X, false); + BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y, false); ECParameterSpec spec = getCurveByJwaId(curveId); ECPoint point = new ECPoint(x, y); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithm.java deleted file mode 100644 index 80ea1257..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithm.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.jsonwebtoken.impl.security; - -import io.jsonwebtoken.Identifiable; -import io.jsonwebtoken.security.CryptoException; -import io.jsonwebtoken.security.CryptoRequest; -import io.jsonwebtoken.security.KeyException; -import io.jsonwebtoken.security.PayloadSupplier; - -import java.security.Key; - -/** - * @since JJWT_RELEASE_VERSION - */ -public interface EncryptionAlgorithm, ERes extends PayloadSupplier, - DReq extends CryptoRequest, DRes extends PayloadSupplier> extends Identifiable { - - ERes encrypt(EReq request) throws CryptoException, KeyException; - - DRes decrypt(DReq request) throws CryptoException, KeyException; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java index a6d3b16b..48989559 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EncryptionAlgorithmsBridge.java @@ -40,7 +40,7 @@ public class EncryptionAlgorithmsBridge { public static SymmetricAeadAlgorithm forId(String id) { SymmetricAeadAlgorithm alg = findById(id); if (alg == null) { - String msg = "Unrecognized JWA EncryptionAlgorithm identifier: " + id; + String msg = "Unrecognized JWA SymmetricAeadAlgorithm identifier: " + id; throw new UnsupportedJwtException(msg); } return alg; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java index 50b75023..8fb8b2e6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithm.java @@ -1,14 +1,16 @@ 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.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.CryptoException; import io.jsonwebtoken.security.KeyException; import io.jsonwebtoken.security.PayloadSupplier; import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest; -import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.SymmetricAeadRequest; import javax.crypto.Cipher; @@ -42,9 +44,9 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl final byte[] iv = ensureInitializationVector(req); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); - byte[] taggedCiphertext = execute(req, Cipher.class, new InstanceCallback() { + byte[] taggedCiphertext = execute(req, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); if (Arrays.length(aad) > 0) { cipher.updateAAD(aad); @@ -61,7 +63,7 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl 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); + return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(), ciphertext, key, aad, tag, iv); } @Override @@ -71,16 +73,16 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl final SecretKey key = assertKey(req); final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); final byte[] aad = getAAD(req); - final byte[] tag = Assert.notEmpty(req.getAuthenticationTag(), "Decryption request authentication tag cannot be null or empty."); + 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 = plus(ciphertext, tag); + final byte[] taggedCiphertext = Bytes.concat(ciphertext, tag); - byte[] plaintext = execute(req, Cipher.class, new InstanceCallback() { + byte[] plaintext = execute(req, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); if (Arrays.length(aad) > 0) { cipher.updateAAD(aad); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java index 6c13f7e8..8b3e116b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.CryptoRequest; @@ -46,12 +47,12 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA @Override public SecretKey generateKey() { - return new JcaTemplate("AES", null).generateSecretKey(this.keyLength * 2); + return new JcaTemplate("AES", null).generateSecretKey(this.keyBitLength * 2); } byte[] assertKeyBytes(CryptoRequest request) { SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); - return validateLength(key, this.keyLength * 2, true); + return validateLength(key, this.keyBitLength * 2, true); } @Override @@ -70,9 +71,9 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA final byte[] iv = ensureInitializationVector(req); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); - final byte[] ciphertext = execute(req, Cipher.class, new InstanceCallback() { + final byte[] ciphertext = execute(req, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec); return cipher.doFinal(plaintext); } @@ -135,7 +136,7 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); final byte[] aad = getAAD(req); - final byte[] tag = assertTag(req.getAuthenticationTag()); + final byte[] tag = assertTag(req.getDigest()); final byte[] iv = assertDecryptionIv(req); final AlgorithmParameterSpec ivSpec = getIvSpec(iv); @@ -147,9 +148,9 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA throw new SignatureException(msg); } - byte[] plaintext = execute(req, Cipher.class, new InstanceCallback() { + byte[] plaintext = execute(req, Cipher.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Cipher cipher) throws Exception { + public byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec); return cipher.doFinal(ciphertext); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/InstanceCallback.java b/impl/src/main/java/io/jsonwebtoken/impl/security/InstanceCallback.java deleted file mode 100644 index 017c0d4e..00000000 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/InstanceCallback.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jsonwebtoken.impl.security; - -public interface InstanceCallback { - - O doWithInstance(I instance) throws Exception; -} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java new file mode 100644 index 00000000..81a836d3 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaPbeKey.java @@ -0,0 +1,51 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.PbeKey; + +import javax.crypto.interfaces.PBEKey; +import javax.security.auth.DestroyFailedException; + +public class JcaPbeKey implements PbeKey { + + private final PBEKey jcaKey; + + public JcaPbeKey(PBEKey jcaKey) { + this.jcaKey = Assert.notNull(jcaKey, "PBEKey cannot be null."); + } + + @Override + public char[] getPassword() { + return this.jcaKey.getPassword(); + } + + @Override + public int getWorkFactor() { + return this.jcaKey.getIterationCount(); + } + + @Override + public String getAlgorithm() { + return this.jcaKey.getAlgorithm(); + } + + @Override + public String getFormat() { + return this.jcaKey.getFormat(); + } + + @Override + public byte[] getEncoded() { + return this.jcaKey.getEncoded(); + } + + @Override + public void destroy() throws DestroyFailedException { + this.jcaKey.destroy(); + } + + @Override + public boolean isDestroyed() { + return this.jcaKey.isDestroyed(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java index 7c4f3364..9bc8965d 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JcaTemplate.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.security.CryptoException; @@ -32,34 +33,34 @@ public class JcaTemplate { this.secureRandom = Assert.notNull(secureRandom, "SecureRandom cannot be null."); } - public T execute(Class clazz, InstanceCallback callback) throws CryptoException { - return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), callback); + public R execute(Class clazz, CheckedFunction fn) throws CryptoException { + return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), fn); } - public SecretKey generateSecretKey(final int keyLength) { - return execute(KeyGenerator.class, new InstanceCallback() { + public SecretKey generateSecretKey(final int keyBitLength) { + return execute(KeyGenerator.class, new CheckedFunction() { @Override - public SecretKey doWithInstance(KeyGenerator generator) { - generator.init(keyLength, secureRandom); + public SecretKey apply(KeyGenerator generator) { + generator.init(keyBitLength, secureRandom); return generator.generateKey(); } }); } - public KeyPair generateKeyPair(final int keyLength) { - return execute(KeyPairGenerator.class, new InstanceCallback() { + public KeyPair generateKeyPair(final int keyBitLength) { + return execute(KeyPairGenerator.class, new CheckedFunction() { @Override - public KeyPair doWithInstance(KeyPairGenerator generator) { - generator.initialize(keyLength, secureRandom); + public KeyPair apply(KeyPairGenerator generator) { + generator.initialize(keyBitLength, secureRandom); return generator.generateKeyPair(); } }); } - private T execute(JcaInstanceSupplier supplier, InstanceCallback callback) throws CryptoException { + private R execute(JcaInstanceSupplier supplier, CheckedFunction callback) throws CryptoException { try { - I instance = supplier.getInstance(); - return callback.doWithInstance(instance); + T instance = supplier.getInstance(); + return callback.apply(instance); } catch (SecurityException se) { throw se; //propagate } catch (Exception e) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java index 54e40492..d6c84ac8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/JwkContext.java @@ -7,34 +7,11 @@ import java.security.Key; import java.security.Provider; import java.security.PublicKey; import java.security.cert.X509Certificate; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; -public interface JwkContext extends Identifiable { - - int size(); - - boolean isEmpty(); - - boolean containsKey(String key); - - boolean containsValue(Object value); - - Object get(String key); - - Set keySet(); - - Collection values(); - - Set> entrySet(); - - Map getValues(); - - Object put(String name, Object value); - - JwkContext putAll(Map m); +public interface JwkContext extends Identifiable, Map { JwkContext setId(String id); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java index 1ab4610c..ddffa27e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyAlgorithmsBridge.java @@ -1,16 +1,29 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.DefaultJweHeader; import io.jsonwebtoken.impl.IdRegistry; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.EncryptionAlgorithms; import io.jsonwebtoken.security.KeyAlgorithm; +import io.jsonwebtoken.security.KeyRequest; +import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SymmetricAeadAlgorithm; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSource; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.MGF1ParameterSpec; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.KeyAlgorithms implementation public final class KeyAlgorithmsBridge { @@ -65,4 +78,153 @@ public final class KeyAlgorithmsBridge { } return instance; } + + 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.getKey().getWorkFactor(); + 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(KeyRequest request) throws SecurityException { + throw new UnsupportedOperationException("Not intended to be called."); + } + + @Override + public String getId() { + return alg.getId(); + } + }; + } + + 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; + + // This is used by `alg` to generate an encryption key during the PBE attempt. While technically the time to + // generate this key during the alg call is not part of the hashing time and shouldn't be counted towards + // desiredMillis, in practice, this is so fast (about ~ 3 milliseconds total aggregated across all + // NUM_SAMPLES on a developer laptop), it is in practice negligible, so we won't need to adjust our + // timing logic below to account for this. + SymmetricAeadAlgorithm encAlg = EncryptionAlgorithms.A128GCM; + + // Strip away all things that cause time during computation except for the actual hashing algorithm: + if (alg instanceof Pbes2HsAkwAlgorithm) { + alg = lean((Pbes2HsAkwAlgorithm) alg); //strip out everything except for the computation we care about + } + + 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++) { + + PbeKey pbeKey = Keys.forPbe().setPassword("12345678").setWorkFactor(workFactor).build(); + KeyRequest request = new DefaultKeyRequest<>(null, null, null, pbeKey, new DefaultJweHeader(), encAlg); + + 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/KeyedRequest.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyedRequest.java new file mode 100644 index 00000000..14f34cbd --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeyedRequest.java @@ -0,0 +1,24 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeySupplier; +import io.jsonwebtoken.security.SecurityRequest; + +import java.security.Key; +import java.security.Provider; +import java.security.SecureRandom; + +public class KeyedRequest extends DefaultSecurityRequest implements SecurityRequest, KeySupplier { + + private final K key; + + public KeyedRequest(Provider provider, SecureRandom secureRandom, K key) { + super(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/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java new file mode 100644 index 00000000..a6828389 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -0,0 +1,16 @@ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.PbeKey; +import io.jsonwebtoken.security.PbeKeyBuilder; + +@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation +public class KeysBridge { + + // prevent instantiation + private KeysBridge() { + } + + public static PbeKeyBuilder forPbe() { + return new DefaultPbeKeyBuilder<>(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java index 57985a1d..abcd949f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/MacSignatureAlgorithm.java @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security; +import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; @@ -134,9 +135,9 @@ public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm request) throws Exception { - return execute(request, Mac.class, new InstanceCallback() { + return execute(request, Mac.class, new CheckedFunction() { @Override - public byte[] doWithInstance(Mac mac) throws Exception { + 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/Pbes2HsAkwAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java index 4a6e42ef..658f7f5a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithm.java @@ -1,17 +1,17 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.JweHeader; -import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.impl.lang.Bytes; -import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.impl.lang.CheckedFunction; +import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.EncryptedKeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; +import io.jsonwebtoken.security.PbeKey; import io.jsonwebtoken.security.SecurityException; +import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; @@ -21,10 +21,11 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; -public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKeyAlgorithm { +public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm { private static final String SALT_HEADER_NAME = "p2s"; private static final String ITERATION_HEADER_NAME = "p2c"; // iteration count + private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2 private final int HASH_BYTE_LENGTH; private final int DERIVED_KEY_BIT_LENGTH; @@ -50,6 +51,16 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey return "PBES2-HS" + hashBitLength + "+" + wrapAlg.getId(); } + public static int assertIterations(int iterations) { + if (iterations < MIN_RECOMMENDED_ITERATIONS) { + String msg = "[JWA RFC 7518, Section 4.8.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2) " + + "recommends password-based-encryption iterations be greater than or equal to " + + MIN_RECOMMENDED_ITERATIONS + ". Provided: " + iterations; + throw new IllegalArgumentException(msg); + } + return iterations; + } + public Pbes2HsAkwAlgorithm(int keyBitLength) { this(hashBitLength(keyBitLength), new AesWrapKeyAlgorithm(keyBitLength)); } @@ -70,63 +81,63 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey this.SALT_PREFIX = toRfcSaltPrefix(getId().getBytes(StandardCharsets.UTF_8)); } - private SecretKey deriveKey(final KeyRequest request, final PBEKey pbeKey, final byte[] salt, final int iterations) { - return execute(request, SecretKeyFactory.class, new InstanceCallback() { - @Override - public SecretKey doWithInstance(SecretKeyFactory factory) throws Exception { - PBEKeySpec spec = null; - try { - spec = new PBEKeySpec(pbeKey.getPassword(), salt, iterations, DERIVED_KEY_BIT_LENGTH); - return factory.generateSecret(spec); - } finally { - if (spec != null) { - spec.clearPassword(); - } - } + // protected visibility for testing + protected SecretKey deriveKey(SecretKeyFactory factory, final char[] password, final byte[] rfcSalt, int iterations) throws Exception { + PBEKeySpec spec = null; + try { + spec = new PBEKeySpec(password, rfcSalt, iterations, DERIVED_KEY_BIT_LENGTH); + return factory.generateSecret(spec); + } finally { + if (spec != null) { + spec.clearPassword(); } - }); + } } - protected byte[] generateInputSalt(KeyRequest request) { + private SecretKey deriveKey(final KeyRequest request, final char[] password, final byte[] salt, final int iterations) { + try { + return execute(request, SecretKeyFactory.class, new CheckedFunction() { + @Override + public SecretKey apply(SecretKeyFactory factory) throws Exception { + return deriveKey(factory, password, salt, iterations); + } + }); + } finally { + if (password != null) { + 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 { + public KeyResult getEncryptionKey(KeyRequest request) throws SecurityException { Assert.notNull(request, "request cannot be null."); - final SecretKey cek = Assert.notNull(request.getPayload(), "request.getPayload() (content encryption key) cannot be null."); - - SecretKey reqKey = request.getKey(); - Assert.notNull(reqKey, "request.getKey() cannot be null."); - if (!(reqKey instanceof PBEKey)) { - String msg = "request.getKey() must be a " + PBEKey.class.getName() + " instance. Type found: " + - reqKey.getClass().getName(); - throw new IllegalArgumentException(msg); - } - final PBEKey pbeKey = (PBEKey) reqKey; - // we explicitly do not attempt to validate pbeKey.getPassword() at this point because that call will create - // a clone of the char array, and we'd have to guarantee cleanup of that clone if any failure/exception occurs. - // Instead, we access the password in only one place - in the execute* method call below - and guarantee - // cleanup there in a try/finally block - - final int iterations = pbeKey.getIterationCount(); - if (iterations < 1000) { - String msg = "Password-based encryption password iterations must be >= 1000. Found: " + iterations; - throw new IllegalArgumentException(msg); - } + SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); + final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key."); + final PbeKey pbeKey = Assert.notNull(request.getKey(), "request.getKey() cannot be null."); + final int iterations = assertIterations(pbeKey.getWorkFactor()); byte[] inputSalt = generateInputSalt(request); - final byte[] rfcSalt = Bytes.plus(this.SALT_PREFIX, inputSalt); + final byte[] rfcSalt = toRfcSalt(inputSalt); final String p2s = Encoders.BASE64URL.encode(inputSalt); - - final SecretKey derivedKek = deriveKey(request, pbeKey, rfcSalt, iterations); + char[] password = pbeKey.getPassword(); // will be safely cleaned/zeroed in deriveKey next: + final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); // now encrypt (wrap) the CEK with the PBE-derived key: DefaultKeyRequest wrapReq = new DefaultKeyRequest<>(request.getProvider(), - request.getSecureRandom(), cek, derivedKek, request.getHeader()); + request.getSecureRandom(), cek, derivedKek, request.getHeader(), request.getEncryptionAlgorithm()); KeyResult result = wrapAlg.getEncryptionKey(wrapReq); request.getHeader().put(SALT_HEADER_NAME, p2s); @@ -138,30 +149,25 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey private static char[] toChars(byte[] bytes) { ByteBuffer buf = ByteBuffer.wrap(bytes); CharBuffer cbuf = StandardCharsets.UTF_8.decode(buf); - char[] chars = new char[cbuf.limit()]; - cbuf.get(chars); + char[] chars = cbuf.compact().array(); return chars; } - private PBEKey toPBEKey(SecretKey key, int iterations) { + private char[] toPasswordChars(SecretKey key) { if (key instanceof PBEKey) { - return (PBEKey) key; + return ((PBEKey) key).getPassword(); } + if (key instanceof PbeKey) { + return ((PbeKey) key).getPassword(); + } + // convert bytes to UTF-8 characters: byte[] keyBytes = null; - char[] keyChars = null; try { keyBytes = key.getEncoded(); - keyChars = toChars(keyBytes); - return new DefaultPBEKey(keyChars, iterations, getId()); + return toChars(keyBytes); } finally { - try { - if (keyChars != null) { - java.util.Arrays.fill(keyChars, '\u0000'); - } - } finally { - if (keyBytes != null) { - java.util.Arrays.fill(keyBytes, (byte) 0); - } + if (keyBytes != null) { + java.util.Arrays.fill(keyBytes, (byte) 0); } } } @@ -170,48 +176,18 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey public SecretKey getDecryptionKey(KeyRequest request) throws SecurityException { JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); - String name = SALT_HEADER_NAME; + final SecretKey key = Assert.notNull(request.getKey(), "Request Key cannot be null."); - Object value = header.get(name); - if (value == null) { - String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value."; - throw new MalformedJwtException(msg); - } - if (!(value instanceof String)) { - String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be a Base64URL-encoded String. Actual type: " + value.getClass().getName(); - throw new MalformedJwtException(msg); - } - String encoded = (String) value; + ValueGetter getter = new DefaultValueGetter(header); + final byte[] inputSalt = getter.getRequiredBytes(SALT_HEADER_NAME); + final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt); + final int iterations = getter.getRequiredPositiveInteger(ITERATION_HEADER_NAME); + final char[] password = toPasswordChars(key); // will be safely cleaned/zeroed in deriveKey next: - final byte[] inputSalt = Decoders.BASE64URL.decode(encoded); - if (Arrays.length(inputSalt) == 0) { - String msg = "The " + getId() + " Key Management Algorithm does not support empty JweHeader '" + name + "' values."; - throw new MalformedJwtException(msg); - } - final byte[] rfcSalt = Bytes.plus(SALT_PREFIX, inputSalt); - - name = ITERATION_HEADER_NAME; - value = header.get(name); - if (value == null) { - String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value."; - throw new MalformedJwtException(msg); - } - if (!(value instanceof Integer)) { - String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be an integer. Actual type: " + value.getClass().getName(); - throw new MalformedJwtException(msg); - } - final int iterations = (Integer) value; - if (iterations <= 0) { - String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be a positive integer. Actual value: " + iterations; - throw new MalformedJwtException(msg); - } - - PBEKey pbeKey = toPBEKey(request.getKey(), iterations); - - final SecretKey derivedKek = deriveKey(request, pbeKey, rfcSalt, iterations); + final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations); KeyRequest unwrapReq = new DefaultKeyRequest<>(request.getProvider(), - request.getSecureRandom(), request.getPayload(), derivedKek, header); + request.getSecureRandom(), request.getPayload(), derivedKek, header, request.getEncryptionAlgorithm()); return wrapAlg.getDecryptionKey(unwrapReq); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java index 97441ee2..6c0ce4e2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaPrivateJwkFactory.java @@ -3,6 +3,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.Converter; import io.jsonwebtoken.impl.lang.Converters; +import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; @@ -127,7 +128,8 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory ctx) { - final BigInteger privateExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.PRIVATE_EXPONENT, true); + final ValueGetter getter = new DefaultValueGetter(ctx); + final BigInteger privateExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIVATE_EXPONENT, true); //The [JWA Spec, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires //RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully: @@ -155,11 +157,11 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory ctx) { - - BigInteger modulus = getRequiredBigInt(ctx, DefaultRsaPublicJwk.MODULUS, false); - BigInteger publicExponent = getRequiredBigInt(ctx, DefaultRsaPublicJwk.PUBLIC_EXPONENT, false); + ValueGetter getter = new DefaultValueGetter(ctx); + BigInteger modulus = getter.getRequiredBigInt(DefaultRsaPublicJwk.MODULUS, false); + BigInteger publicExponent = getter.getRequiredBigInt(DefaultRsaPublicJwk.PUBLIC_EXPONENT, false); final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent); RSAPublicKey key = generateKey(ctx, new CheckedFunction() { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java index 663425f4..7cb524c0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/SecretJwkFactory.java @@ -1,10 +1,9 @@ package io.jsonwebtoken.impl.security; -import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.impl.lang.ValueGetter; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.security.MalformedKeyException; import io.jsonwebtoken.security.SecretJwk; import io.jsonwebtoken.security.UnsupportedKeyException; @@ -58,18 +57,8 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory { @Override protected SecretJwk createJwkFromValues(JwkContext ctx) { - String encoded = getRequiredString(ctx, DefaultSecretJwk.K); - byte[] bytes; - try { - bytes = Decoders.BASE64URL.decode(encoded); - if (Arrays.length(bytes) == 0) { - throw new IllegalArgumentException("JWK 'k' member does not have any encoded bytes. JWK: {" + ctx + "}"); - } - } catch (Exception e) { - String msg = "Unable to Base64Url-decode " + DefaultSecretJwk.TYPE_VALUE + - " JWK 'k' member value. JWK: {" + ctx + "}"; - throw new MalformedKeyException(msg, e); - } + ValueGetter getter = new DefaultValueGetter(ctx); + byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K); SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here? ctx.setKey(key); return new DefaultSecretJwk(ctx); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy index ae9b541b..7e1db8a1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweBuilderTest.groovy @@ -2,14 +2,10 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.EncryptionAlgorithms -import io.jsonwebtoken.security.KeyAlgorithm -import io.jsonwebtoken.security.KeyAlgorithms -import io.jsonwebtoken.security.SymmetricAeadAlgorithm import org.junit.Test -import java.security.Key - -import static org.junit.Assert.* +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class DefaultJweBuilderTest { @@ -51,7 +47,7 @@ class DefaultJweBuilderTest { try { builder().setIssuer("me").withKey(key).compact() } catch (IllegalStateException ise) { - assertEquals 'EncryptionAlgorithm is required.', ise.message + assertEquals 'Encryption algorithm is required.', ise.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index ac976896..c6dfc95e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -2,24 +2,24 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Arrays - -import javax.crypto.SecretKey -import java.nio.charset.StandardCharsets - -import static org.junit.Assert.* - import io.jsonwebtoken.security.EncryptionAlgorithms import org.junit.Test import javax.crypto.Cipher +import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec +import java.nio.charset.StandardCharsets + +import static org.junit.Assert.* class AesGcmKeyAlgorithmTest { /** - * This tests asserts that our EncyrptionAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm + * This tests asserts that our SymmetricAeadAlgorithm 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. */ @@ -35,9 +35,9 @@ class AesGcmKeyAlgorithmTest { def cek = alg.generateKey(); JcaTemplate template = new JcaTemplate("AES/GCM/NoPadding", null) - byte[] jcaResult = template.execute(Cipher.class, new InstanceCallback() { + byte[] jcaResult = template.execute(Cipher.class, new CheckedFunction() { @Override - byte[] doWithInstance(Cipher cipher) throws Exception { + byte[] apply(Cipher cipher) throws Exception { cipher.init(Cipher.WRAP_MODE, kek, new GCMParameterSpec(128, iv)) return cipher.wrap(cek) } @@ -55,7 +55,7 @@ class AesGcmKeyAlgorithmTest { def encRequest = new DefaultSymmetricAeadRequest(null, null, cek.getEncoded(), kek, null, iv) def encResult = EncryptionAlgorithms.A256GCM.encrypt(encRequest) - assertArrayEquals resultA.authenticationTag, encResult.authenticationTag + assertArrayEquals resultA.digest, encResult.digest assertArrayEquals resultA.initializationVector, encResult.initializationVector assertArrayEquals resultA.payload, encResult.payload } @@ -70,16 +70,21 @@ class AesGcmKeyAlgorithmTest { def header = new DefaultJweHeader() def kek = template.generateSecretKey(keyLength) def cek = template.generateSecretKey(keyLength) + def enc = new GcmAesAeadAlgorithm(keyLength) { + @Override + SecretKey generateKey() { + return cek; + } + } - def ereq = new DefaultKeyRequest(null, null, cek, kek, header) + def ereq = new DefaultKeyRequest(null, null, cek, kek, header, enc) def result = alg.getEncryptionKey(ereq) - header.putAll(result.getHeaderParams()) byte[] encryptedKeyBytes = result.getPayload() assertFalse "encryptedKey must be populated", Arrays.length(encryptedKeyBytes) == 0 - def dcek = alg.getDecryptionKey(new DefaultKeyRequest(null, null, encryptedKeyBytes, kek, header)) + def dcek = alg.getDecryptionKey(new DefaultKeyRequest(null, null, encryptedKeyBytes, kek, header, enc)) //Assert the decrypted key matches the original cek assertEquals cek.algorithm, dcek.algorithm @@ -100,16 +105,21 @@ class AesGcmKeyAlgorithmTest { def header = new DefaultJweHeader() def kek = template.generateSecretKey(keyLength) def cek = template.generateSecretKey(keyLength) - def ereq = new DefaultKeyRequest(null, null, cek, kek, header) + def enc = new GcmAesAeadAlgorithm(keyLength) { + @Override + SecretKey generateKey() { + return cek + } + } + def ereq = new DefaultKeyRequest(null, null, cek, kek, header, enc) def result = alg.getEncryptionKey(ereq) - header.putAll(result.getHeaderParams()) header.put(headerName, value) //null value will remove it byte[] encryptedKeyBytes = result.getPayload() try { - alg.getDecryptionKey(new DefaultKeyRequest(null, null, encryptedKeyBytes, kek, header)) + alg.getDecryptionKey(new DefaultKeyRequest(null, null, encryptedKeyBytes, kek, header, enc)) fail() } catch (MalformedJwtException iae) { assertEquals exmsg, iae.getMessage() @@ -117,17 +127,16 @@ class AesGcmKeyAlgorithmTest { } String missing(String name) { - return "The A128GCMKW Key Management Algorithm requires a JweHeader '${name}' value." as String + return "JWE header is missing required '${name}' value." as String } - String type(String name) { - return "The A128GCMKW Key Management Algorithm requires the JweHeader '${name}' value to be a Base64URL-encoded String. Actual type: java.lang.Integer" as String + return "JWE header '${name}' value must be a String. Actual type: java.lang.Integer" as String } String base64Url(String name) { - return "JweHeader '${name}' value 'T#ZW@#' does not appear to be a valid Base64URL String: Illegal base64url character: '#'" + return "JWE header '${name}' value is not a valid Base64URL String: Illegal base64url character: '#'" } - String length(String name, int requiredLen) { - return "The 'A128GCMKW' key management algorithm requires the JweHeader '${name}' value to be ${requiredLen * Byte.SIZE} bits (${requiredLen} bytes) in length. Actual length: 16 bits (2 bytes)." + 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 @@ -151,7 +160,7 @@ class AesGcmKeyAlgorithmTest { @Test void testIncorrectLengths() { def value = Encoders.BASE64URL.encode("hi".getBytes(StandardCharsets.US_ASCII)) - testDecryptionHeader('iv', value, length('iv', 12)) - testDecryptionHeader('tag', value, length('tag', 16)) + testDecryptionHeader('iv', value, length('iv', 96)) + testDecryptionHeader('tag', value, length('tag', 128)) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy deleted file mode 100644 index 292513dc..00000000 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJweFactoryTest.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package io.jsonwebtoken.impl.security - -import org.junit.Test - -/** - * @since JJWT_RELEASE_VERSION - */ -class DefaultJweFactoryTest { - - @Test - void testDefaultCtor() { - new DefaultJweFactory() - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy index cbb701f5..a28fe51d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DirectKeyAlgorithmTest.groovy @@ -21,7 +21,7 @@ class DirectKeyAlgorithmTest { void testGetEncryptionKey() { def alg = new DirectKeyAlgorithm() def key = new SecretKeySpec(new byte[1], "AES") - def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader()) + def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null) def result = alg.getEncryptionKey(request) assertSame key, result.getKey() assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key @@ -35,7 +35,7 @@ class DirectKeyAlgorithmTest { @Test(expected = IllegalArgumentException) void testGetEncryptionKeyWithNullRequestKey() { def key = new SecretKeySpec(new byte[1], "AES") - def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader()) { + def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null) { @Override Key getKey() { return null @@ -48,7 +48,7 @@ class DirectKeyAlgorithmTest { void testGetDecryptionKey() { def alg = new DirectKeyAlgorithm() def key = new SecretKeySpec(new byte[1], "AES") - def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader()) + def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null) def result = alg.getDecryptionKey(request) assertSame key, result } @@ -61,7 +61,7 @@ class DirectKeyAlgorithmTest { @Test(expected = IllegalArgumentException) void testGetDecryptionKeyWithNullRequestKey() { def key = new SecretKeySpec(new byte[1], "AES") - def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader()) { + def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null) { @Override Key getKey() { return null diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy index e273d3d7..eb575c89 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/GcmAesAeadAlgorithmTest.groovy @@ -51,7 +51,7 @@ class GcmAesAeadAlgorithmTest { def result = alg.encrypt(req) byte[] ciphertext = result.getPayload() - byte[] tag = result.getAuthenticationTag() + byte[] tag = result.getDigest() byte[] iv = result.getInitializationVector() assertArrayEquals E, ciphertext diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy index 8f0f726a..c992c4c8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithmTest.groovy @@ -33,7 +33,7 @@ class HmacAesAeadAlgorithmTest { def req = new DefaultSymmetricAeadRequest(null, null, plaintext, key, null) def result = alg.encrypt(req); - def realTag = result.getAuthenticationTag(); + def realTag = result.getDigest(); //fake it: def fakeTag = new byte[realTag.length] diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy index 9eb710b0..7832efbf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JcaTemplateTest.groovy @@ -1,5 +1,6 @@ package io.jsonwebtoken.impl.security +import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.security.CryptoException import io.jsonwebtoken.security.SignatureException import org.junit.Test @@ -18,9 +19,9 @@ class JcaTemplateTest { void testNewCipherWithExplicitProvider() { Provider provider = Security.getProvider('SunJCE') def template = new JcaTemplate('AES/CBC/PKCS5Padding', provider) - template.execute(Cipher.class, new InstanceCallback() { + template.execute(Cipher.class, new CheckedFunction() { @Override - byte[] doWithInstance(Cipher cipher) throws Exception { + byte[] apply(Cipher cipher) throws Exception { assertNotNull cipher assertSame provider, cipher.provider return new byte[0] @@ -97,9 +98,9 @@ class JcaTemplateTest { def ex = new Exception("testing") def template = new JcaTemplate('AES/CBC/PKCS5Padding', null) try { - template.execute(Cipher.class, new InstanceCallback() { + template.execute(Cipher.class, new CheckedFunction() { @Override - byte[] doWithInstance(Cipher cipher) throws Exception { + byte[] apply(Cipher cipher) throws Exception { throw ex } }) 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..d5c89721 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pbes2HsAkwAlgorithmTest.groovy @@ -0,0 +1,44 @@ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.DefaultJweHeader +import io.jsonwebtoken.security.EncryptionAlgorithms +import io.jsonwebtoken.security.KeyAlgorithms +import io.jsonwebtoken.security.Keys +import org.junit.Test + +import java.nio.charset.StandardCharsets + +class Pbes2HsAkwAlgorithmTest { + + @Test + void test() { + + def alg = KeyAlgorithms.PBES2_HS256_A128KW + + int desiredMillis = 200 + int iterations = KeyAlgorithms.estimateIterations(alg, desiredMillis) + println "Estimated iterations: $iterations" + + int tries = 30 + int skip = 6 + //double scale = 0.5035246727 + + def payload = 'hello world'.getBytes(StandardCharsets.UTF_8) + def key = Keys.forPbe().setPassword('hellowor').setWorkFactor(iterations).build() + def req = new DefaultKeyRequest(null, null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.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/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index b30a75b0..ae93c526 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -1,6 +1,5 @@ package io.jsonwebtoken.impl.security - import io.jsonwebtoken.Jwe import io.jsonwebtoken.JweHeader import io.jsonwebtoken.Jwts @@ -8,6 +7,8 @@ import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.security.KeyRequest +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.PbeKey import io.jsonwebtoken.security.SecurityRequest import org.junit.Test @@ -303,11 +304,13 @@ class RFC7517AppendixCTest { } } + PbeKey pbeKey = Keys.forPbe().setPassword(RFC_SHARED_PASSPHRASE).setWorkFactor(RFC_P2C).build() + String compact = Jwts.jweBuilder() .setPayload(RFC_JWK_JSON) .setHeaderParam('cty', 'jwk+json') .encryptWith(encAlg) - .withKeyFrom(RFC_SHARED_PASSPHRASE.toCharArray(), RFC_P2C, keyAlg) + .withKeyFrom(pbeKey, keyAlg) .serializeToJsonWith(serializer) //ensure JJWT created the header as expected with an assertion serializer .compact(); diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy index 643ca889..69062651 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB1Test.groovy @@ -74,7 +74,7 @@ class RFC7518AppendixB1Test { def result = alg.encrypt(request); byte[] ciphertext = result.getPayload() - byte[] tag = result.getAuthenticationTag() + byte[] tag = result.getDigest() byte[] iv = result.getInitializationVector() assertArrayEquals E, ciphertext diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy index db6aaf71..22074d68 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB2Test.groovy @@ -74,7 +74,7 @@ class RFC7518AppendixB2Test { AeadResult result = alg.encrypt(req) byte[] resultCiphertext = result.getPayload() - byte[] resultTag = result.getAuthenticationTag() + byte[] resultTag = result.getDigest() byte[] resultIv = result.getInitializationVector() assertArrayEquals E, resultCiphertext diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy index 25f8d67f..26311711 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixB3Test.groovy @@ -74,7 +74,7 @@ class RFC7518AppendixB3Test { AeadResult result = alg.encrypt(req) byte[] resultCiphertext = result.getPayload() - byte[] resultTag = result.getAuthenticationTag(); + byte[] resultTag = result.getDigest(); byte[] resultIv = result.getInitializationVector(); assertArrayEquals E, resultCiphertext diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy index d3080377..851bc9b2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/EncryptionAlgorithmsTest.groovy @@ -49,7 +49,7 @@ class EncryptionAlgorithmsTest { def result = alg.encrypt(request) - byte[] tag = result.getAuthenticationTag() //there is always a tag, even if there is no AAD + byte[] tag = result.getDigest() //there is always a tag, even if there is no AAD assertNotNull tag byte[] ciphertext = result.getPayload() @@ -87,7 +87,7 @@ class EncryptionAlgorithmsTest { assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) } - def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getAuthenticationTag(), result.getInitializationVector()); + 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) }