diff --git a/ninjax-core/src/main/java/org/r10r/ninjax/core/NinjaSessionConverter.java b/ninjax-core/src/main/java/org/r10r/ninjax/core/NinjaSessionConverter.java index 0d447ef..c812ffc 100644 --- a/ninjax-core/src/main/java/org/r10r/ninjax/core/NinjaSessionConverter.java +++ b/ninjax-core/src/main/java/org/r10r/ninjax/core/NinjaSessionConverter.java @@ -35,6 +35,17 @@ public NinjaSessionConverter(NinjaProperties ninjaProperties) { }); byte[] decodedKey = Base64.getDecoder().decode(encodedSecret); + + // HS256 requires a key of at least 256 bits (32 bytes). A shorter key (e.g. the + // 'changeme' demo default) would still "work" with this custom Jwts implementation but + // produces weak, forgeable tokens. Fail fast at startup instead of silently accepting it. + int MINIMUM_SECRET_LENGTH_IN_BYTES = 32; + if (decodedKey.length < MINIMUM_SECRET_LENGTH_IN_BYTES) { + throw new RuntimeException(String.format( + "The secret '%s' in 'conf/application.conf' is too weak. HS256 requires at least %d bytes (256 bits) after Base64-decoding, but the configured secret decodes to %d bytes. Please generate a strong secret using 'mvn ninja:generateSecret'.", + NinjaConstants.NINJA_APPLICATION_SECRET_KEY, MINIMUM_SECRET_LENGTH_IN_BYTES, decodedKey.length)); + } + this.secretKeyForSessionEncryption = new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256"); this.sessionExpiryTimeInSeconds = ninjaProperties.get("application.session.expire_time_in_seconds").map(v -> Long.valueOf(v)); diff --git a/ninjax-core/src/test/java/org/r10r/ninjax/core/NinjaSessionConverterTest.java b/ninjax-core/src/test/java/org/r10r/ninjax/core/NinjaSessionConverterTest.java new file mode 100644 index 0000000..5b73199 --- /dev/null +++ b/ninjax-core/src/test/java/org/r10r/ninjax/core/NinjaSessionConverterTest.java @@ -0,0 +1,88 @@ +package org.r10r.ninjax.core; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.r10r.ninjax.core.properties.NinjaProperties; + +public class NinjaSessionConverterTest { + + /** + * Test double for NinjaProperties that serves a fixed map of properties instead of + * loading from conf/application.conf. + */ + private static class FixedNinjaProperties extends NinjaProperties { + private final Map values; + + FixedNinjaProperties(Map values) { + this.values = values; + } + + @Override + public Optional get(String propertyName) { + return Optional.ofNullable(values.get(propertyName)); + } + } + + private static String base64SecretOfLength(int numberOfBytes) { + return Base64.getEncoder().encodeToString(new byte[numberOfBytes]); + } + + @Test + public void shouldRejectSecretShorterThan32Bytes() { + // given + NinjaProperties properties = new FixedNinjaProperties( + Map.of(NinjaConstants.NINJA_APPLICATION_SECRET_KEY, base64SecretOfLength(31))); + + // when + RuntimeException exception = assertThrows(RuntimeException.class, + () -> new NinjaSessionConverter(properties)); + + // then + assertThat(exception.getMessage()).contains("too weak"); + } + + @Test + public void shouldRejectWeakChangemeDemoDefault() { + // given + NinjaProperties properties = new FixedNinjaProperties( + Map.of(NinjaConstants.NINJA_APPLICATION_SECRET_KEY, "changeme")); + + // when + RuntimeException exception = assertThrows(RuntimeException.class, + () -> new NinjaSessionConverter(properties)); + + // then + assertThat(exception.getMessage()).contains("too weak"); + } + + @Test + public void shouldAcceptSecretOfAtLeast32Bytes() { + // given + NinjaProperties properties = new FixedNinjaProperties( + Map.of(NinjaConstants.NINJA_APPLICATION_SECRET_KEY, base64SecretOfLength(32))); + + // when + NinjaSessionConverter converter = new NinjaSessionConverter(properties); + + // then + assertThat(converter).isNotNull(); + } + + @Test + public void shouldFailWhenSecretIsMissing() { + // given + NinjaProperties properties = new FixedNinjaProperties(Map.of()); + + // when + RuntimeException exception = assertThrows(RuntimeException.class, + () -> new NinjaSessionConverter(properties)); + + // then + assertThat(exception.getMessage()).contains("Missing key"); + } +} diff --git a/ninjax-demo-todo/src/main/java/conf/application.conf b/ninjax-demo-todo/src/main/java/conf/application.conf index 6e7daa6..9a77022 100644 --- a/ninjax-demo-todo/src/main/java/conf/application.conf +++ b/ninjax-demo-todo/src/main/java/conf/application.conf @@ -1,5 +1,6 @@ ninja.port=8081 -application.secret=changeme +# Demo secret only. Generate your own for production with 'mvn ninja:generateSecret'. +application.secret=RHwx0fWz73AlJx1fulfkrYKL5Yo7t8F1H8xUajByE28= application.session.expire_time_in_seconds=3600 application.session.cookie.secure=false diff --git a/ninjax-demo-todo/src/test/resources/conf/application.conf b/ninjax-demo-todo/src/test/resources/conf/application.conf index 1f03122..cc263a1 100644 --- a/ninjax-demo-todo/src/test/resources/conf/application.conf +++ b/ninjax-demo-todo/src/test/resources/conf/application.conf @@ -1,6 +1,6 @@ # Set in test: ninja.port= -application.secret=changeme +application.secret=RHwx0fWz73AlJx1fulfkrYKL5Yo7t8F1H8xUajByE28= application.session.expire_time_in_seconds=3600 application.session.cookie.secure=false diff --git a/ninjax-jetty-demo-todo/src/main/java/conf/application.conf b/ninjax-jetty-demo-todo/src/main/java/conf/application.conf index 6e7daa6..9a77022 100644 --- a/ninjax-jetty-demo-todo/src/main/java/conf/application.conf +++ b/ninjax-jetty-demo-todo/src/main/java/conf/application.conf @@ -1,5 +1,6 @@ ninja.port=8081 -application.secret=changeme +# Demo secret only. Generate your own for production with 'mvn ninja:generateSecret'. +application.secret=RHwx0fWz73AlJx1fulfkrYKL5Yo7t8F1H8xUajByE28= application.session.expire_time_in_seconds=3600 application.session.cookie.secure=false diff --git a/ninjax-jetty-demo-todo/src/test/resources/conf/application.conf b/ninjax-jetty-demo-todo/src/test/resources/conf/application.conf index 1f03122..cc263a1 100644 --- a/ninjax-jetty-demo-todo/src/test/resources/conf/application.conf +++ b/ninjax-jetty-demo-todo/src/test/resources/conf/application.conf @@ -1,6 +1,6 @@ # Set in test: ninja.port= -application.secret=changeme +application.secret=RHwx0fWz73AlJx1fulfkrYKL5Yo7t8F1H8xUajByE28= application.session.expire_time_in_seconds=3600 application.session.cookie.secure=false