diff --git a/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java b/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java index cdeb8ac..680b160 100644 --- a/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java +++ b/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java @@ -66,7 +66,12 @@ public HmacOneTimePasswordGenerator() { * 6 and 8, inclusive */ public HmacOneTimePasswordGenerator(final int passwordLength) { - this(passwordLength, HOTP_HMAC_ALGORITHM); + // Every implementation of the Java platform is required to support the HmacSHA1 Mac algorithm, so we don't need + // to check for a `NoSuchAlgorithm` exception + this.algorithm = HOTP_HMAC_ALGORITHM; + this.modDivisor = getModDivisor(passwordLength); + this.formatString = getFormatString(passwordLength); + this.passwordLength = passwordLength; } /** @@ -80,47 +85,59 @@ public HmacOneTimePasswordGenerator(final int passwordLength) { * HOTP only allows for {@value com.eatthepath.otp.HmacOneTimePasswordGenerator#HOTP_HMAC_ALGORITHM}, but derived * standards like TOTP may allow for other algorithms * - * @throws UncheckedNoSuchAlgorithmException if the given algorithm is not supported by the underlying JRE + * @throws NoSuchAlgorithmException if the given algorithm is not supported by the underlying JRE */ - HmacOneTimePasswordGenerator(final int passwordLength, final String algorithm) throws UncheckedNoSuchAlgorithmException { - try { - // Fail fast if the requested algorithm isn't supported - final Mac mac = Mac.getInstance(algorithm); + HmacOneTimePasswordGenerator(final int passwordLength, final String algorithm) throws NoSuchAlgorithmException { + // Fail fast if the requested algorithm isn't supported + final Mac mac = Mac.getInstance(algorithm); + + assert mac.getMacLength() >= 8; - if (mac.getMacLength() < 8) { - throw new IllegalArgumentException(algorithm + " has MAC length less than 8 bytes"); + this.algorithm = algorithm; + + this.modDivisor = getModDivisor(passwordLength); + this.formatString = getFormatString(passwordLength); + this.passwordLength = passwordLength; + } + + private static int getModDivisor(final int passwordLength) { + switch (passwordLength) { + case 6: { + return 1_000_000; } - this.algorithm = algorithm; - } catch (final NoSuchAlgorithmException e) { - throw new UncheckedNoSuchAlgorithmException(e); + case 7: { + return 10_000_000; + } + + case 8: { + return 100_000_000; + } + + default: { + throw new IllegalArgumentException("Password length must be between 6 and 8 digits."); + } } + } + private static String getFormatString(final int passwordLength) { switch (passwordLength) { case 6: { - this.modDivisor = 1_000_000; - this.formatString = "%06d"; - break; + return "%06d"; } case 7: { - this.modDivisor = 10_000_000; - this.formatString = "%07d"; - break; + return "%07d"; } case 8: { - this.modDivisor = 100_000_000; - this.formatString = "%08d"; - break; + return "%08d"; } default: { throw new IllegalArgumentException("Password length must be between 6 and 8 digits."); } } - - this.passwordLength = passwordLength; } /** diff --git a/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java b/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java index 07e3a1a..40dd5ed 100644 --- a/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java +++ b/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java @@ -23,9 +23,11 @@ import javax.crypto.Mac; import java.security.InvalidKeyException; import java.security.Key; +import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.time.Instant; import java.util.Locale; +import java.util.stream.Stream; /** *

Generates time-based one-time passwords (TOTP) as specified in @@ -94,7 +96,13 @@ public TimeBasedOneTimePasswordGenerator(final Duration timeStep) { * 6 and 8, inclusive */ public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int passwordLength) { - this(timeStep, passwordLength, TOTP_ALGORITHM_HMAC_SHA1); + try { + this.hotp = new HmacOneTimePasswordGenerator(passwordLength, TOTP_ALGORITHM_HMAC_SHA1); + } catch (final NoSuchAlgorithmException e) { + throw new AssertionError("Every implementation of the Java platform is required to support the HmacSHA1 Mac algorithm", e); + } + + this.timeStep = validateTimeStep(timeStep); } /** @@ -108,8 +116,10 @@ public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int pass * for {@value #TOTP_ALGORITHM_HMAC_SHA1}, {@value #TOTP_ALGORITHM_HMAC_SHA256}, and * {@value #TOTP_ALGORITHM_HMAC_SHA512} * - * @throws UncheckedNoSuchAlgorithmException if the given algorithm is {@value #TOTP_ALGORITHM_HMAC_SHA512} and the - * JVM does not support that algorithm; all JVMs are required to support {@value #TOTP_ALGORITHM_HMAC_SHA1} and + * @throws IllegalArgumentException if the given algorithm is not an algorithm allowed by RFC 6238 (i.e. + * {@value #TOTP_ALGORITHM_HMAC_SHA1}, {@value TOTP_ALGORITHM_HMAC_SHA256}, or {@value TOTP_ALGORITHM_HMAC_SHA512}) + * @throws NoSuchAlgorithmException if the given algorithm is {@value #TOTP_ALGORITHM_HMAC_SHA512} and the JVM does + * not support that algorithm; all JVMs are required to support {@value #TOTP_ALGORITHM_HMAC_SHA1} and * {@value #TOTP_ALGORITHM_HMAC_SHA256}, but are not required to support {@value #TOTP_ALGORITHM_HMAC_SHA512} * * @see #TOTP_ALGORITHM_HMAC_SHA1 @@ -117,14 +127,24 @@ public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int pass * @see #TOTP_ALGORITHM_HMAC_SHA512 */ public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int passwordLength, final String algorithm) - throws UncheckedNoSuchAlgorithmException { + throws NoSuchAlgorithmException { + + if (Stream.of(TOTP_ALGORITHM_HMAC_SHA1, TOTP_ALGORITHM_HMAC_SHA256, TOTP_ALGORITHM_HMAC_SHA512) + .noneMatch(supportedAlgorithm -> supportedAlgorithm.equals(algorithm))) { + + throw new IllegalArgumentException("TOTP requires an algorithm of \"HmacSHA1\", \"HmacSHA256\", or \"HmacSHA512\""); + } + this.hotp = new HmacOneTimePasswordGenerator(passwordLength, algorithm); + this.timeStep = validateTimeStep(timeStep); + } + + private static Duration validateTimeStep(final Duration timeStep) { if (timeStep.toMillis() <= 0) { throw new IllegalArgumentException("Time step must be at least 1 millisecond"); } - this.hotp = new HmacOneTimePasswordGenerator(passwordLength, algorithm); - this.timeStep = timeStep; + return timeStep; } /** diff --git a/src/main/java/com/eatthepath/otp/UncheckedNoSuchAlgorithmException.java b/src/main/java/com/eatthepath/otp/UncheckedNoSuchAlgorithmException.java deleted file mode 100644 index b8322c3..0000000 --- a/src/main/java/com/eatthepath/otp/UncheckedNoSuchAlgorithmException.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.eatthepath.otp; - -import java.security.NoSuchAlgorithmException; - -/** - * Wraps a {@link NoSuchAlgorithmException} with an unchecked exception. - * - * @author Jon Chambers - */ -public class UncheckedNoSuchAlgorithmException extends RuntimeException { - - /** - * Constructs a new unchecked {@code NoSuchAlgorithmException} instance. - * - * @param cause the underlying {@code NoSuchAlgorithmException} - */ - UncheckedNoSuchAlgorithmException(final NoSuchAlgorithmException cause) { - super(cause); - } - - /** - * Returns the underlying {@link NoSuchAlgorithmException} that caused this exception. - * - * @return the underlying {@link NoSuchAlgorithmException} that caused this exception - */ - @Override - public NoSuchAlgorithmException getCause() { - return (NoSuchAlgorithmException) super.getCause(); - } -} diff --git a/src/test/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorTest.java b/src/test/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorTest.java index e1c0cb6..0c95b72 100644 --- a/src/test/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorTest.java +++ b/src/test/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorTest.java @@ -29,7 +29,7 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.Key; -import java.time.Instant; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Locale; import java.util.concurrent.*; @@ -70,10 +70,8 @@ void hmacOneTimePasswordGeneratorWithLongPasswordLength() { @Test void hmacOneTimePasswordGeneratorWithBogusAlgorithm() { - final UncheckedNoSuchAlgorithmException exception = assertThrows(UncheckedNoSuchAlgorithmException.class, () -> - new HmacOneTimePasswordGenerator(6, "Definitely not a real algorithm")); - - assertNotNull(exception.getCause()); + assertThrows(NoSuchAlgorithmException.class, () -> + new HmacOneTimePasswordGenerator(6, "Definitely not a real algorithm")); } @Test @@ -83,7 +81,7 @@ void getPasswordLength() { } @Test - void getAlgorithm() { + void getAlgorithm() throws NoSuchAlgorithmException { final String algorithm = "HmacSHA256"; assertEquals(algorithm, new HmacOneTimePasswordGenerator(6, algorithm).getAlgorithm()); } diff --git a/src/test/java/com/eatthepath/otp/TimeBasedOneTimePasswordGeneratorTest.java b/src/test/java/com/eatthepath/otp/TimeBasedOneTimePasswordGeneratorTest.java index 845b83f..f47887e 100644 --- a/src/test/java/com/eatthepath/otp/TimeBasedOneTimePasswordGeneratorTest.java +++ b/src/test/java/com/eatthepath/otp/TimeBasedOneTimePasswordGeneratorTest.java @@ -55,6 +55,12 @@ class TimeBasedOneTimePasswordGeneratorTest { private static final byte[] HMAC_SHA512_KEY_BYTES = "1234567890123456789012345678901234567890123456789012345678901234".getBytes(StandardCharsets.US_ASCII); + @Test + void timeBasedOneTimePasswordGeneratorWithUnsupportedAlgorithm() { + assertThrows(IllegalArgumentException.class, + () -> new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 6, "Not a supported algorithm")); + } + @Test void timeBasedOneTimePasswordGeneratorWithNonPositiveTimeStamp() { assertThrows(IllegalArgumentException.class, () -> new TimeBasedOneTimePasswordGenerator(Duration.ZERO)); @@ -69,7 +75,7 @@ void getPasswordLength() { } @Test - void getAlgorithm() { + void getAlgorithm() throws NoSuchAlgorithmException { final String algorithm = TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256; assertEquals(algorithm, new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 6, algorithm).getAlgorithm()); }