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 efabdf2a..691ac6f3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java @@ -176,6 +176,16 @@ public final class Bytes { return output; } + /** + * Clears the array by filling it with all zeros. Does nothing with a null or empty argument. + * + * @param bytes the (possibly null or empty) byte array to clear + */ + public static void clear(byte[] bytes) { + if (isEmpty(bytes)) return; + java.util.Arrays.fill(bytes, (byte) 0); + } + public static boolean isEmpty(byte[] bytes) { return length(bytes) == 0; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java index 5b2f4709..35373e17 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java @@ -17,10 +17,12 @@ package io.jsonwebtoken.impl.lang; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Registry; import java.math.BigInteger; import java.net.URI; +import java.security.MessageDigest; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Date; @@ -97,4 +99,46 @@ public final class Fields { newFields.put(id, field); // add new one return registry(newFields.values()); } + + private static byte[] bytes(BigInteger i) { + return i != null ? i.toByteArray() : null; + } + + public static boolean bytesEquals(BigInteger a, BigInteger b) { + //noinspection NumberEquality + if (a == b) return true; + if (a == null || b == null) return false; + byte[] aBytes = bytes(a); + byte[] bBytes = bytes(b); + try { + return MessageDigest.isEqual(aBytes, bBytes); + } finally { + Bytes.clear(aBytes); + Bytes.clear(bBytes); + } + } + + private static boolean equals(T a, T b, Field field) { + if (a == b) return true; + if (a == null || b == null) return false; + if (field.isSecret()) { + // byte[] and BigInteger are the only types of secret Fields in the JJWT codebase + // (i.e. Field.isSecret() == true). If a Field is ever marked as secret, and it's not one of these two + // data types, we need to know about it. So we use the 'assertSecret' helper above to ensure we do: + if (a instanceof byte[]) { + return b instanceof byte[] && MessageDigest.isEqual((byte[]) a, (byte[]) b); + } else if (a instanceof BigInteger) { + return b instanceof BigInteger && bytesEquals((BigInteger) a, (BigInteger) b); + } + } + // default to a standard null-safe comparison: + return Objects.nullSafeEquals(a, b); + } + + public static boolean equals(FieldReadable a, Object o, Field field) { + if (a == o) return true; + if (a == null || !(o instanceof FieldReadable)) return false; + FieldReadable b = (FieldReadable) o; + return equals(a.get(field), b.get(field), field); + } } 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 33b5ce23..913a04af 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwk.java @@ -33,6 +33,9 @@ import io.jsonwebtoken.security.KeyOperation; import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -48,10 +51,11 @@ public abstract class AbstractJwk implements Jwk, FieldReadabl .set().setId("key_ops").setName("Key Operations").build(); static final Field KTY = Fields.string("kty", "Key Type"); static final Set> FIELDS = Collections.setOf(ALG, KID, KEY_OPS, KTY); - public static final String IMMUTABLE_MSG = "JWKs are immutable and may not be modified."; + protected final JwkContext context; private final List> THUMBPRINT_FIELDS; + private final int hashCode; /** * @param ctx the backing JwkContext containing the JWK field values. @@ -71,6 +75,40 @@ public abstract class AbstractJwk implements Jwk, FieldReadabl String kid = thumbprint.toString(); ctx.setId(kid); } + this.hashCode = computeHashCode(); + } + + /** + * Compute and return the JWK hashCode. As JWKs are immutable, this value will be cached as a final constant + * upon JWK instantiation. This uses the JWK's thumbprint fields during computation, but differs from JwkThumbprint + * calculation in two ways: + *
    + *
  1. JwkThumbprints use a MessageDigest calculation, which is unnecessary overhead for a hashcode
  2. + *
  3. The hashCode calculation uses each field's idiomatic (Java) object value instead of the + * JwkThumbprint-required canonical (String) value.
  4. + *
+ * + * @return the JWK hashcode + */ + private int computeHashCode() { + List list = new ArrayList<>(this.THUMBPRINT_FIELDS.size() + 1 /* possible discriminator */); + // So we don't leak information about the private key value, we need a discriminator to ensure that + // public and private key hashCodes are not identical (in case both JWKs need to be in the same hash set). + // So we add a discriminator String to the list of values that are used during hashCode calculation + Key key = Assert.notNull(toKey(), "JWK toKey() value cannot be null."); + if (key instanceof PublicKey) { + list.add("Public"); + } else if (key instanceof PrivateKey) { + list.add("Private"); + } + for (Field field : this.THUMBPRINT_FIELDS) { + // Unlike thumbprint calculation, we get the idiomatic (Java) value, not canonical (String) value + // (We could have used either actually, but the idiomatic value hashCode calculation is probably + // faster). + Object val = Assert.notNull(get(field), "computeHashCode: Field idiomatic value cannot be null."); + list.add(val); + } + return Objects.nullSafeHashCode(list.toArray()); } private String getRequiredThumbprintValue(Field field) { @@ -230,13 +268,20 @@ public abstract class AbstractJwk implements Jwk, FieldReadabl } @Override - public int hashCode() { - return this.context.hashCode(); + public final int hashCode() { + return this.hashCode; } - @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override - public boolean equals(Object obj) { - return this.context.equals(obj); + public final boolean equals(Object obj) { + if (obj == this) return true; + if (obj instanceof Jwk) { + Jwk other = (Jwk) obj; + // this.getType() guaranteed non-null in constructor: + return getType().equals(other.getType()) && equals(other); + } + return false; } + + protected abstract boolean equals(Jwk jwk); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java index d75628d6..276800e5 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java @@ -170,6 +170,12 @@ abstract class AbstractJwkBuilder, T extends Jwk implements SecretJwkBuilder { public DefaultSecretJwkBuilder(JwkContext ctx) { super(ctx); + // assign a standard algorithm if possible: + Key key = Assert.notNull(ctx.getKey(), "SecretKey cannot be null."); + DefaultMacAlgorithm mac = DefaultMacAlgorithm.findByKey(key); + if (mac != null) { + algorithm(mac.getId()); + } } } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java index e0ea3cf3..2884657e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPrivateJwk.java @@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.KeyPair; import io.jsonwebtoken.security.PrivateJwk; import io.jsonwebtoken.security.PublicJwk; @@ -47,4 +48,11 @@ abstract class AbstractPrivateJwk toKeyPair() { return this.keyPair; } + + @Override + protected final boolean equals(Jwk jwk) { + return jwk instanceof PrivateJwk && equals((PrivateJwk) jwk); + } + + protected abstract boolean equals(PrivateJwk jwk); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java index 7319fe05..a5df02a7 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractPublicJwk.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.PublicJwk; import java.security.PublicKey; @@ -25,4 +26,11 @@ abstract class AbstractPublicJwk extends AbstractAsymmetric AbstractPublicJwk(JwkContext ctx, List> thumbprintFields) { super(ctx, thumbprintFields); } + + @Override + protected final boolean equals(Jwk jwk) { + return jwk instanceof PublicJwk && equals((PublicJwk) jwk); + } + + protected abstract boolean equals(PublicJwk jwk); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java index 33a9a1c2..9dfa86f8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPrivateJwk.java @@ -20,12 +20,15 @@ import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.EcPrivateJwk; import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.PrivateJwk; import java.math.BigInteger; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.util.Set; +import static io.jsonwebtoken.impl.security.DefaultEcPublicJwk.equalsPublic; + class DefaultEcPrivateJwk extends AbstractPrivateJwk implements EcPrivateJwk { static final Field D = Fields.secretBigInt("d", "ECC Private Key"); @@ -38,4 +41,9 @@ class DefaultEcPrivateJwk extends AbstractPrivateJwk jwk) { + return jwk instanceof EcPrivateJwk && equalsPublic(this, jwk) && Fields.equals(this, jwk, D); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java index 907b6659..9336ae72 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultEcPublicJwk.java @@ -16,9 +16,11 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.EcPublicJwk; +import io.jsonwebtoken.security.PublicJwk; import java.math.BigInteger; import java.security.interfaces.ECPublicKey; @@ -39,4 +41,15 @@ class DefaultEcPublicJwk extends AbstractPublicJwk implements EcPub DefaultEcPublicJwk(JwkContext ctx) { super(ctx, THUMBPRINT_FIELDS); } + + static boolean equalsPublic(FieldReadable self, Object candidate) { + return Fields.equals(self, candidate, CRV) && + Fields.equals(self, candidate, X) && + Fields.equals(self, candidate, Y); + } + + @Override + protected boolean equals(PublicJwk jwk) { + return jwk instanceof EcPublicJwk && equalsPublic(this, jwk); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java index 169f33b3..f72c169b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java @@ -53,7 +53,7 @@ final class DefaultMacAlgorithm extends AbstractSecureDigestAlgorithm JCA_NAME_MAP; + private static final Map JCA_NAME_MAP; static { JCA_NAME_MAP = new LinkedHashMap<>(6); @@ -96,7 +96,7 @@ final class DefaultMacAlgorithm extends AbstractSecureDigestAlgorithm extends AbstractPrivateJwk> implements OctetPrivateJwk { +import static io.jsonwebtoken.impl.security.DefaultOctetPublicJwk.equalsPublic; + +public class DefaultOctetPrivateJwk + extends AbstractPrivateJwk> implements OctetPrivateJwk { static final Field D = Fields.bytes("d", "The private key").setSecret(true).build(); @@ -37,4 +41,9 @@ public class DefaultOctetPrivateJwk e // https://www.rfc-editor.org/rfc/rfc7638#section-3.2.1 DefaultOctetPublicJwk.THUMBPRINT_FIELDS, pubJwk); } + + @Override + protected boolean equals(PrivateJwk jwk) { + return jwk instanceof OctetPrivateJwk && equalsPublic(this, jwk) && Fields.equals(this, jwk, D); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java index 4982d3ae..4ede32e1 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultOctetPublicJwk.java @@ -16,9 +16,11 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.security.OctetPublicJwk; +import io.jsonwebtoken.security.PublicJwk; import java.security.PublicKey; import java.util.List; @@ -37,4 +39,13 @@ public class DefaultOctetPublicJwk extends AbstractPublicJw DefaultOctetPublicJwk(JwkContext ctx) { super(ctx, THUMBPRINT_FIELDS); } + + static boolean equalsPublic(FieldReadable self, Object candidate) { + return Fields.equals(self, candidate, CRV) && Fields.equals(self, candidate, X); + } + + @Override + protected boolean equals(PublicJwk jwk) { + return jwk instanceof OctetPublicJwk && equalsPublic(this, jwk); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java index 384b4b20..bc908dcf 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwk.java @@ -16,8 +16,10 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.PrivateJwk; import io.jsonwebtoken.security.RsaPrivateJwk; import io.jsonwebtoken.security.RsaPublicJwk; @@ -28,6 +30,8 @@ import java.security.spec.RSAOtherPrimeInfo; import java.util.List; import java.util.Set; +import static io.jsonwebtoken.impl.security.DefaultRsaPublicJwk.equalsPublic; + class DefaultRsaPrivateJwk extends AbstractPrivateJwk implements RsaPrivateJwk { static final Field PRIVATE_EXPONENT = Fields.secretBigInt("d", "Private Exponent"); @@ -54,4 +58,39 @@ class DefaultRsaPrivateJwk extends AbstractPrivateJwk aOthers = a.get(OTHER_PRIMES_INFO); + List bOthers = b.get(OTHER_PRIMES_INFO); + int aSize = Collections.size(aOthers); + int bSize = Collections.size(bOthers); + if (aSize != bSize) return false; + if (aSize == 0) return true; + RSAOtherPrimeInfo[] aInfos = aOthers.toArray(new RSAOtherPrimeInfo[0]); + RSAOtherPrimeInfo[] bInfos = bOthers.toArray(new RSAOtherPrimeInfo[0]); + for (int i = 0; i < aSize; i++) { + if (!equals(aInfos[i], bInfos[i])) return false; + } + return true; + } + + @Override + protected boolean equals(PrivateJwk jwk) { + return jwk instanceof RsaPrivateJwk && equalsPublic(this, jwk) && + Fields.equals(this, jwk, PRIVATE_EXPONENT) && + Fields.equals(this, jwk, FIRST_PRIME) && + Fields.equals(this, jwk, SECOND_PRIME) && + Fields.equals(this, jwk, FIRST_CRT_EXPONENT) && + Fields.equals(this, jwk, SECOND_CRT_EXPONENT) && + Fields.equals(this, jwk, FIRST_CRT_COEFFICIENT) && + equalsOtherPrimes(this, (FieldReadable) jwk); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java index 000c9e82..5357bdcc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultRsaPublicJwk.java @@ -16,8 +16,10 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.PublicJwk; import io.jsonwebtoken.security.RsaPublicJwk; import java.math.BigInteger; @@ -38,4 +40,13 @@ class DefaultRsaPublicJwk extends AbstractPublicJwk implements Rsa DefaultRsaPublicJwk(JwkContext ctx) { super(ctx, THUMBPRINT_FIELDS); } + + static boolean equalsPublic(FieldReadable self, Object candidate) { + return Fields.equals(self, candidate, MODULUS) && Fields.equals(self, candidate, PUBLIC_EXPONENT); + } + + @Override + protected boolean equals(PublicJwk jwk) { + return jwk instanceof RsaPublicJwk && equalsPublic(this, jwk); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java index 22be55cf..8ca1e170 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretJwk.java @@ -18,6 +18,7 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.impl.lang.Field; import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.SecretJwk; import javax.crypto.SecretKey; @@ -36,4 +37,9 @@ class DefaultSecretJwk extends AbstractJwk implements SecretJwk { DefaultSecretJwk(JwkContext ctx) { super(ctx, THUMBPRINT_FIELDS); } + + @Override + protected boolean equals(Jwk jwk) { + return jwk instanceof SecretJwk && Fields.equals(this, jwk, K); + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy index fdd6a4fd..2ffe22e1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/AbstractProtectedHeaderTest.groovy @@ -144,7 +144,7 @@ class AbstractProtectedHeaderTest { h([jwk: jwk]) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {kty=oct, k=}. " + + String msg = "Invalid JWT header 'jwk' (JSON Web Key) value: {alg=HS256, kty=oct, k=}. " + "Value must be a Public JWK, not a Secret JWK." assertEquals msg, expected.getMessage() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy index 5c48e921..19bb9a78 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJweHeaderTest.groovy @@ -63,7 +63,7 @@ class DefaultJweHeaderTest { h([epk: values]) fail() } catch (IllegalArgumentException expected) { - String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {kty=oct, k=}. " + + String msg = "Invalid JWE header 'epk' (Ephemeral Public Key) value: {alg=HS256, kty=oct, k=}. " + "Value must be a Public JWK, not a Secret JWK." assertEquals msg, expected.getMessage() } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy index ba8778b1..43f96621 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/BytesTest.groovy @@ -290,4 +290,39 @@ class BytesTest { Bytes.length(-1) } + @Test + void testClearNull() { + Bytes.clear(null) // no exception + } + + @Test + void testClearEmpty() { + Bytes.clear(Bytes.EMPTY) // no exception + } + + @Test + void testClear() { + int len = 16 + byte[] bytes = Bytes.random(len) + boolean allZero = true + for(int i = 0; i < len; i++) { + if (bytes[i] != (byte)0) { + allZero = false + break + } + } + assertFalse allZero // guarantee that we start with random bytes + + Bytes.clear(bytes) + + allZero = true + for(int i = 0; i < len; i++) { + if (bytes[i] != (byte)0) { + allZero = false + break + } + } + assertTrue allZero // asserts zeroed out entirely + } + } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy index ef1869e1..2dd8393b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/FieldsTest.groovy @@ -221,4 +221,70 @@ class FieldsTest { def field = Fields.builder(String.class).setId('foo').setName("FooName").build() assertFalse field.equals(new Object()) } + + @Test + void testBigIntegerBytesNull() { + assertNull Fields.bytes(null) + } + + @Test + void testBytesEqualsWhenBothAreNull() { + assertTrue Fields.bytesEquals(null, null) + } + + @Test + void testBytesEqualsIdentity() { + assertTrue Fields.bytesEquals(BigInteger.ONE, BigInteger.ONE) + } + + @Test + void testBytesEqualsWhenAIsNull() { + assertFalse Fields.bytesEquals(null, BigInteger.ONE) + } + + @Test + void testBytesEqualsWhenBIsNull() { + assertFalse Fields.bytesEquals(BigInteger.ONE, null) + } + + @Test + void testFieldValueEqualsWhenAIsNull() { + BigInteger a = null + BigInteger b = BigInteger.ONE + Field field = Fields.bigInt('foo', 'bar').build() + assertFalse Fields.equals(a, b, field) + } + + @Test + void testFieldValueEqualsWhenBIsNull() { + BigInteger a = BigInteger.ONE + BigInteger b = null + Field field = Fields.bigInt('foo', 'bar').build() + assertFalse Fields.equals(a, b, field) + } + + @Test + void testFieldValueEqualsSecretString() { + String a = 'hello' + String b = new String('hello'.toCharArray()) // new instance not in the string table (Groovy side effect) + Field field = Fields.builder(String.class).setId('foo').setName('bar').setSecret(true).build() + assertTrue Fields.equals(a, b, field) + } + + @Test + void testEqualsIdentity() { + FieldReadable r = new TestFieldReadable() + assertTrue Fields.equals(r, r, Fields.string('foo', 'bar')) + } + + @Test + void testEqualsWhenAIsNull() { + assertFalse Fields.equals(null, "hello", Fields.string('foo', 'bar')) + } + + @Test + void testEqualsWhenAIsFieldReadableButBIsNot() { + FieldReadable r = new TestFieldReadable() + assertFalse Fields.equals(r, "hello", Fields.string('foo', 'bar')) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy index a4686e51..7928fada 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/RedactedSupplierTest.groovy @@ -17,8 +17,7 @@ package io.jsonwebtoken.impl.lang import org.junit.Test -import static org.junit.Assert.assertFalse -import static org.junit.Assert.assertTrue +import static org.junit.Assert.* class RedactedSupplierTest { @@ -43,4 +42,16 @@ class RedactedSupplierTest { assertFalse new RedactedSupplier<>(42).equals(new RedactedSupplier(30)) } + @Test + void testEqualsIdentity() { + def supplier = new RedactedSupplier('hello') + assertEquals supplier, supplier + } + + @Test + void testHashCode() { + int hashCode = 42.hashCode() + assertEquals hashCode, new RedactedSupplier(42).hashCode() + } + } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/TestFieldReadable.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/TestFieldReadable.groovy new file mode 100644 index 00000000..67bb16e3 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/TestFieldReadable.groovy @@ -0,0 +1,26 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.lang + +class TestFieldReadable implements FieldReadable { + + def value = null + + @Override + Object get(Field field) { + return value + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy index 88a3b7ba..5db61d0e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkTest.groovy @@ -16,7 +16,9 @@ package io.jsonwebtoken.impl.security import io.jsonwebtoken.lang.Collections +import io.jsonwebtoken.security.Jwk import io.jsonwebtoken.security.Jwks +import io.jsonwebtoken.security.SecretJwk import org.junit.Before import org.junit.Test @@ -44,7 +46,12 @@ class AbstractJwkTest { } static AbstractJwk newJwk(JwkContext ctx) { - return new AbstractJwk(ctx, Collections.of(AbstractJwk.KTY)) {} + return new AbstractJwk(ctx, Collections.of(AbstractJwk.KTY)) { + @Override + protected boolean equals(Jwk jwk) { + return this.@context.equals(jwk.@context) + } + } } @Before @@ -144,24 +151,22 @@ class AbstractJwkTest { @Test void testPrivateJwkHashCode() { - assertEquals jwk.hashCode(), jwk.@context.hashCode() - def secretJwk1 = Jwks.builder().key(TestKeys.HS256).add('hello', 'world').build() def secretJwk2 = Jwks.builder().key(TestKeys.HS256).add('hello', 'world').build() - assertEquals secretJwk1.hashCode(), secretJwk1.@context.hashCode() - assertEquals secretJwk2.hashCode(), secretJwk2.@context.hashCode() assertEquals secretJwk1.hashCode(), secretJwk2.hashCode() def ecPrivJwk1 = Jwks.builder().key(TestKeys.ES256.pair.private).add('hello', 'ecworld').build() def ecPrivJwk2 = Jwks.builder().key(TestKeys.ES256.pair.private).add('hello', 'ecworld').build() assertEquals ecPrivJwk1.hashCode(), ecPrivJwk2.hashCode() - assertEquals ecPrivJwk1.hashCode(), ecPrivJwk1.@context.hashCode() - assertEquals ecPrivJwk2.hashCode(), ecPrivJwk2.@context.hashCode() def rsaPrivJwk1 = Jwks.builder().key(TestKeys.RS256.pair.private).add('hello', 'rsaworld').build() def rsaPrivJwk2 = Jwks.builder().key(TestKeys.RS256.pair.private).add('hello', 'rsaworld').build() assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk2.hashCode() - assertEquals rsaPrivJwk1.hashCode(), rsaPrivJwk1.@context.hashCode() - assertEquals rsaPrivJwk2.hashCode(), rsaPrivJwk2.@context.hashCode() + } + + @Test + void testEqualsWithNonJwk() { + SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() + assertFalse jwk.equals(42) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwkTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwkTest.groovy new file mode 100644 index 00000000..72682b28 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaPrivateJwkTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.FieldReadable +import io.jsonwebtoken.impl.lang.TestFieldReadable +import org.junit.Test + +import java.security.spec.RSAOtherPrimeInfo + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue + +class DefaultRsaPrivateJwkTest { + + @Test + void testEqualsOtherPrimesDifferentSizes() { + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.TEN, BigInteger.TEN, BigInteger.TEN) + FieldReadable a = new TestFieldReadable(value: [info1, info2]) + FieldReadable b = new TestFieldReadable(value: [info1]) // different sizes + assertFalse DefaultRsaPrivateJwk.equalsOtherPrimes(a, b) + } + + @Test + void testEqualsOtherPrimes() { + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + FieldReadable a = new TestFieldReadable(value: [info1]) + FieldReadable b = new TestFieldReadable(value: [info2]) + assertTrue DefaultRsaPrivateJwk.equalsOtherPrimes(a, b) + } + + @Test + void testEqualsOtherPrimesIdentity() { + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + FieldReadable a = new TestFieldReadable(value: [info1]) + FieldReadable b = new TestFieldReadable(value: [info1]) + assertTrue DefaultRsaPrivateJwk.equalsOtherPrimes(a, b) + } + + @Test + void testEqualsOtherPrimesNullElement() { + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + // sizes are the same, but one element is null: + FieldReadable a = new TestFieldReadable(value: [null]) + FieldReadable b = new TestFieldReadable(value: [info1]) + assertFalse DefaultRsaPrivateJwk.equalsOtherPrimes(a, b) + } + + @Test + void testEqualsOtherPrimesInfoNotEqual() { + def info1 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.ONE) + def info2 = new RSAOtherPrimeInfo(BigInteger.ONE, BigInteger.ONE, BigInteger.TEN) // different + FieldReadable a = new TestFieldReadable(value: [info1]) + FieldReadable b = new TestFieldReadable(value: [info2]) + assertFalse DefaultRsaPrivateJwk.equalsOtherPrimes(a, b) + } + +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy index 42aee874..b8e6d208 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy @@ -55,14 +55,14 @@ class JwksTest { //test non-null value: //noinspection GroovyAssignabilityCheck - def builder = Jwks.builder().key(key) + def builder = Jwks.builder().key(key).delete('alg') // delete alg put there by SecretKeyBuilder builder."$name"(val) def jwk = builder.build() assertEquals val, jwk."get${cap}"() assertEquals expectedFieldValue, jwk."${id}" //test null value: - builder = Jwks.builder().key(key) + builder = Jwks.builder().key(key).delete('alg') try { builder."$name"(null) fail("IAE should have been thrown") @@ -74,7 +74,7 @@ class JwksTest { assertFalse jwk.containsKey(id) //test empty string value - builder = Jwks.builder().key(key) + builder = Jwks.builder().key(key).delete('alg') if (val instanceof String) { try { builder."$name"(' ' as String) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy index ad3921cf..a47a2c2b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/SecretJwkFactoryTest.groovy @@ -33,7 +33,7 @@ class SecretJwkFactoryTest { @Test // if a jwk does not have an 'alg' or 'use' field, we default to an AES key void testNoAlgNoSigJcaName() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk assertEquals 'AES', result.toKey().getAlgorithm() } @@ -41,13 +41,13 @@ class SecretJwkFactoryTest { @Test void testJwkHS256AlgSetsKeyJcaNameCorrectly() { SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() - SecretJwk result = Jwks.builder().add(jwk).add('alg', 'HS256').build() as SecretJwk + SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk assertEquals 'HmacSHA256', result.toKey().getAlgorithm() } @Test void testSignOpSetsKeyHmacSHA256() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') @@ -57,13 +57,13 @@ class SecretJwkFactoryTest { @Test void testJwkHS384AlgSetsKeyJcaNameCorrectly() { SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build() - SecretJwk result = Jwks.builder().add(jwk).add('alg', 'HS384').build() as SecretJwk + SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk assertEquals 'HmacSHA384', result.toKey().getAlgorithm() } @Test void testSignOpSetsKeyHmacSHA384() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build() SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') @@ -73,13 +73,13 @@ class SecretJwkFactoryTest { @Test void testJwkHS512AlgSetsKeyJcaNameCorrectly() { SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build() - SecretJwk result = Jwks.builder().add(jwk).add('alg', 'HS512').build() as SecretJwk + SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk assertEquals 'HmacSHA512', result.toKey().getAlgorithm() } @Test void testSignOpSetsKeyHmacSHA512() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build() SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk assertNull result.getAlgorithm() assertNull result.get('use') @@ -89,7 +89,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256 void testNoAlgAndSigUseForHS256() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk @@ -99,7 +99,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384 void testNoAlgAndSigUseForHS384() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk @@ -109,7 +109,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512 void testNoAlgAndSigUseForHS512() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk @@ -119,7 +119,7 @@ class SecretJwkFactoryTest { @Test // no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES void testNoAlgAndNonSigUse() { - SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() + SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build() assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('use') SecretJwk result = Jwks.builder().add(jwk).add('use', 'foo').build() as SecretJwk