Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 39 additions & 22 deletions src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* <p>Generates time-based one-time passwords (TOTP) as specified in
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -108,23 +116,35 @@ 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&nbsp;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
* @see #TOTP_ALGORITHM_HMAC_SHA256
* @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;
}

/**
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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
Expand All @@ -83,7 +81,7 @@ void getPasswordLength() {
}

@Test
void getAlgorithm() {
void getAlgorithm() throws NoSuchAlgorithmException {
final String algorithm = "HmacSHA256";
assertEquals(algorithm, new HmacOneTimePasswordGenerator(6, algorithm).getAlgorithm());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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());
}
Expand Down