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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int pass
public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int passwordLength, final String algorithm)
throws UncheckedNoSuchAlgorithmException {

if (timeStep.toMillis() <= 0) {
throw new IllegalArgumentException("Time step must be at least 1 millisecond");
}

this.hotp = new HmacOneTimePasswordGenerator(passwordLength, algorithm);
this.timeStep = timeStep;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;
import java.util.concurrent.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments;

public class HmacOneTimePasswordGeneratorTest {
class HmacOneTimePasswordGeneratorTest {

private static final Key HOTP_KEY =
new SecretKeySpec("12345678901234567890".getBytes(StandardCharsets.US_ASCII),
Expand All @@ -57,31 +58,31 @@ public class HmacOneTimePasswordGeneratorTest {
};

@Test
void testHmacOneTimePasswordGeneratorWithShortPasswordLength() {
void hmacOneTimePasswordGeneratorWithShortPasswordLength() {
assertThrows(IllegalArgumentException.class, () -> new HmacOneTimePasswordGenerator(5));
}

@Test
void testHmacOneTimePasswordGeneratorWithLongPasswordLength() {
void hmacOneTimePasswordGeneratorWithLongPasswordLength() {
assertThrows(IllegalArgumentException.class, () -> new HmacOneTimePasswordGenerator(9));
}

@Test
void testHmacOneTimePasswordGeneratorWithBogusAlgorithm() {
void hmacOneTimePasswordGeneratorWithBogusAlgorithm() {
final UncheckedNoSuchAlgorithmException exception = assertThrows(UncheckedNoSuchAlgorithmException.class, () ->
new HmacOneTimePasswordGenerator(6, "Definitely not a real algorithm"));

assertNotNull(exception.getCause());
}

@Test
void testGetPasswordLength() {
void getPasswordLength() {
final int passwordLength = 7;
assertEquals(passwordLength, new HmacOneTimePasswordGenerator(passwordLength).getPasswordLength());
}

@Test
void testGetAlgorithm() {
void getAlgorithm() {
final String algorithm = "HmacSHA256";
assertEquals(algorithm, new HmacOneTimePasswordGenerator(6, algorithm).getAlgorithm());
}
Expand All @@ -91,55 +92,45 @@ void testGetAlgorithm() {
* <a href="https://tools.ietf.org/html/rfc4226#appendix-D">RFC&nbsp;4226, Appendix D</a>.
*/
@ParameterizedTest
@MethodSource("argumentsForTestGenerateOneTimePasswordHotp")
void testGenerateOneTimePassword(final int counter, final int expectedOneTimePassword) throws Exception {
@MethodSource
void generateOneTimePassword(final int counter, final int expectedOneTimePassword) throws Exception {
assertEquals(expectedOneTimePassword, new HmacOneTimePasswordGenerator().generateOneTimePassword(HOTP_KEY, counter));
}

private static Stream<Arguments> generateOneTimePassword() {
return IntStream.range(0, TEST_VECTORS.length)
.mapToObj(counter -> Arguments.of(counter, TEST_VECTORS[counter]));
}

@Test
void testGenerateOneTimePasswordRepeated() throws Exception {
void generateOneTimePasswordRepeated() throws Exception {
final HmacOneTimePasswordGenerator hotpGenerator = new HmacOneTimePasswordGenerator();

for (int counter = 0; counter < TEST_VECTORS.length; counter++) {
assertEquals(TEST_VECTORS[counter], hotpGenerator.generateOneTimePassword(HOTP_KEY, counter));
}
}

private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordHotp() {
final Stream.Builder<Arguments> streamBuilder = Stream.builder();

for (int counter = 0; counter < TEST_VECTORS.length; counter++) {
streamBuilder.add(Arguments.of(counter, TEST_VECTORS[counter]));
}

return streamBuilder.build();
}

@ParameterizedTest
@MethodSource("argumentsForTestGenerateOneTimePasswordStringHotp")
void testGenerateOneTimePasswordString(final int counter, final String expectedOneTimePassword) throws Exception {
@MethodSource
void generateOneTimePasswordString(final int counter, final String expectedOneTimePassword) throws Exception {
Locale.setDefault(Locale.US);
assertEquals(expectedOneTimePassword, new HmacOneTimePasswordGenerator().generateOneTimePasswordString(HOTP_KEY, counter));
}

private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringHotp() {
final Stream.Builder<Arguments> streamBuilder = Stream.builder();

for (int counter = 0; counter < TEST_VECTORS.length; counter++) {
streamBuilder.add(Arguments.of(counter, String.valueOf(TEST_VECTORS[counter])));
}

return streamBuilder.build();
private static Stream<Arguments> generateOneTimePasswordString() {
return IntStream.range(0, TEST_VECTORS.length)
.mapToObj(counter -> Arguments.of(counter, String.valueOf(TEST_VECTORS[counter])));
}

@ParameterizedTest
@MethodSource("argumentsForTestGenerateOneTimePasswordStringLocaleHotp")
void testGenerateOneTimePasswordStringLocale(final int counter, final Locale locale, final String expectedOneTimePassword) throws Exception {
@MethodSource
void generateOneTimePasswordStringLocale(final int counter, final Locale locale, final String expectedOneTimePassword) throws Exception {
Locale.setDefault(Locale.US);
assertEquals(expectedOneTimePassword, new HmacOneTimePasswordGenerator().generateOneTimePasswordString(HOTP_KEY, counter, locale));
}

private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringLocaleHotp() {
private static Stream<Arguments> generateOneTimePasswordStringLocale() {
final Locale locale = Locale.forLanguageTag("hi-IN-u-nu-Deva");

return Stream.of(
Expand All @@ -157,30 +148,27 @@ private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringLo
}

@Test
void testConcurrent() throws InterruptedException {
void generateOneTimePasswordConcurrent() throws InterruptedException {
final int iterations = 10_000;
final int threadCount = Runtime.getRuntime().availableProcessors() * 4;
final CountDownLatch threadStartLatch = new CountDownLatch(threadCount);
final CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount);
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
@SuppressWarnings("unchecked") final CompletableFuture<Boolean>[] futures = new CompletableFuture[threadCount];

final HmacOneTimePasswordGenerator hotpGenerator = new HmacOneTimePasswordGenerator();

for (int thread = 0; thread < threadCount; thread++) {
futures[thread] = CompletableFuture.supplyAsync(() -> {
final Random random = new Random();
boolean allMatched = true;

threadStartLatch.countDown();

try {
threadStartLatch.await();
} catch (final InterruptedException e) {
cyclicBarrier.await();
} catch (final InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}

for (int i = 0; i < iterations; i++) {
final int counter = random.nextInt(TEST_VECTORS.length);
final int counter = ThreadLocalRandom.current().nextInt(TEST_VECTORS.length);

try {
if (hotpGenerator.generateOneTimePassword(HOTP_KEY, counter) != TEST_VECTORS[counter]) {
Expand All @@ -197,9 +185,7 @@ void testConcurrent() throws InterruptedException {

CompletableFuture.allOf(futures).join();

for (final CompletableFuture<Boolean> future : futures) {
assertTrue(future.join());
}
assertTrue(Arrays.stream(futures).allMatch(CompletableFuture::join));

executorService.shutdown();
assertTrue(executorService.awaitTermination(1, TimeUnit.SECONDS));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;

public class TimeBasedOneTimePasswordGeneratorTest {
class TimeBasedOneTimePasswordGeneratorTest {

private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
Expand All @@ -55,19 +56,26 @@ public class TimeBasedOneTimePasswordGeneratorTest {
"1234567890123456789012345678901234567890123456789012345678901234".getBytes(StandardCharsets.US_ASCII);

@Test
void testGetPasswordLength() {
void timeBasedOneTimePasswordGeneratorWithNonPositiveTimeStamp() {
assertThrows(IllegalArgumentException.class, () -> new TimeBasedOneTimePasswordGenerator(Duration.ZERO));
assertThrows(IllegalArgumentException.class, () -> new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(-1)));
assertThrows(IllegalArgumentException.class, () -> new TimeBasedOneTimePasswordGenerator(Duration.ofNanos(1)));
}

@Test
void getPasswordLength() {
final int passwordLength = 7;
assertEquals(passwordLength, new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), passwordLength).getPasswordLength());
}

@Test
void testGetAlgorithm() {
void getAlgorithm() {
final String algorithm = TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256;
assertEquals(algorithm, new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 6, algorithm).getAlgorithm());
}

@Test
void testGetTimeStep() {
void getTimeStep() {
final Duration timeStep = Duration.ofSeconds(97);
assertEquals(timeStep, new TimeBasedOneTimePasswordGenerator(timeStep).getTimeStep());
}
Expand All @@ -80,8 +88,8 @@ void testGetTimeStep() {
* different keys are used for each of the various HMAC algorithms.
*/
@ParameterizedTest
@MethodSource("argumentsForTestGenerateOneTimePasswordTotp")
void testGenerateOneTimePasswordTotp(final String algorithm, final byte[] keyBytes, final long epochSeconds, final int expectedOneTimePassword) throws Exception {
@MethodSource
void generateOneTimePassword(final String algorithm, final byte[] keyBytes, final long epochSeconds, final int expectedOneTimePassword) throws Exception {
assumeAlgorithmSupported(algorithm);

final TimeBasedOneTimePasswordGenerator totp =
Expand All @@ -93,7 +101,7 @@ void testGenerateOneTimePasswordTotp(final String algorithm, final byte[] keyByt
assertEquals(expectedOneTimePassword, totp.generateOneTimePassword(key, timestamp));
}

private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordTotp() {
private static Stream<Arguments> generateOneTimePassword() {
return Stream.of(
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 59L, 94287082),
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111109L, 7081804),
Expand All @@ -117,8 +125,8 @@ private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordTotp() {
}

@ParameterizedTest
@MethodSource("argumentsForTestGenerateOneTimePasswordStringTotp")
void testGenerateOneTimePasswordStringTotp(final String algorithm, final byte[] keyBytes, final long epochSeconds, final String expectedOneTimePassword) throws Exception {
@MethodSource
void generateOneTimePasswordString(final String algorithm, final byte[] keyBytes, final long epochSeconds, final String expectedOneTimePassword) throws Exception {
assumeAlgorithmSupported(algorithm);

final TimeBasedOneTimePasswordGenerator totp =
Expand All @@ -130,7 +138,7 @@ void testGenerateOneTimePasswordStringTotp(final String algorithm, final byte[]
assertEquals(expectedOneTimePassword, totp.generateOneTimePasswordString(key, timestamp));
}

private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringTotp() {
private static Stream<Arguments> generateOneTimePasswordString() {
return Stream.of(
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 59L, "94287082"),
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111109L, "07081804"),
Expand All @@ -154,8 +162,8 @@ private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringTo
}

@ParameterizedTest
@MethodSource("argumentsForTestGenerateOneTimePasswordStringLocaleTotp")
void testGenerateOneTimePasswordStringLocaleTotp(final String algorithm, final byte[] keyBytes, final long epochSeconds, final Locale locale, final String expectedOneTimePassword) throws Exception {
@MethodSource
void generateOneTimePasswordStringLocale(final String algorithm, final byte[] keyBytes, final long epochSeconds, final Locale locale, final String expectedOneTimePassword) throws Exception {
assumeAlgorithmSupported(algorithm);

final TimeBasedOneTimePasswordGenerator totp =
Expand All @@ -167,7 +175,7 @@ void testGenerateOneTimePasswordStringLocaleTotp(final String algorithm, final b
assertEquals(expectedOneTimePassword, totp.generateOneTimePasswordString(key, timestamp, locale));
}

private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringLocaleTotp() {
private static Stream<Arguments> generateOneTimePasswordStringLocale() {
final Locale locale = Locale.forLanguageTag("hi-IN-u-nu-Deva");

return Stream.of(
Expand Down