From 6ae8b8c4fa6ef07129bf5c916778501e35a8f139 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sat, 18 Apr 2020 15:01:56 +0100 Subject: [PATCH 01/14] initial attempt at solution, can verify if codes are consecutive and valid, but not time difference --- .../totp/code/DefaultCodeVerifier.java | 47 +++++++++++++++++++ .../totp/code/DefaultCodeVerifierTest.java | 18 +++++++ 2 files changed, 65 insertions(+) diff --git a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java index 7fdfd33..39455cf 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java +++ b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java @@ -2,6 +2,7 @@ import dev.samstevens.totp.exceptions.CodeGenerationException; import dev.samstevens.totp.time.TimeProvider; +import java.time.Duration; public class DefaultCodeVerifier implements CodeVerifier { @@ -19,10 +20,19 @@ public void setTimePeriod(int timePeriod) { this.timePeriod = timePeriod; } + public int getTimePeriod() { + return timePeriod; + } + public void setAllowedTimePeriodDiscrepancy(int allowedTimePeriodDiscrepancy) { this.allowedTimePeriodDiscrepancy = allowedTimePeriodDiscrepancy; } + public void setAllowedTimePeriodDuration(Duration duration) { + long periods = duration.getSeconds() / this.getTimePeriod(); + this.setAllowedTimePeriodDiscrepancy((int) periods); + } + @Override public boolean isValidCode(String secret, String code) { // Get the current number of seconds since the epoch and @@ -39,6 +49,43 @@ public boolean isValidCode(String secret, String code) { return success; } + + public boolean areValidCodes(String secret, String... codes) { + // Get the current number of seconds since the epoch and + // calculate the number of time periods passed. + long currentBucket = Math.floorDiv(timeProvider.getTime(), timePeriod); + + // Calculate and compare the codes for all the "valid" time periods, + // even if we get an early match, to avoid timing attacks + boolean success = false; + int successiveMatches = 0; + int currentCodeBeingChecked = 0; + + + boolean isValid; + for (int i = -allowedTimePeriodDiscrepancy; i <= allowedTimePeriodDiscrepancy; i++) { + isValid = checkCode(secret, currentBucket + i, codes[currentCodeBeingChecked]); + if (isValid) { + currentCodeBeingChecked++; + } else { + if (currentCodeBeingChecked > 0) { + isValid = checkCode(secret, currentBucket + i, codes[0]); + if (isValid) { + currentCodeBeingChecked++; + } + } + } + + System.out.println(currentCodeBeingChecked); + if (!success && currentCodeBeingChecked == codes.length) { + success = true; + currentCodeBeingChecked = 0; + } + } + + return success; + } + /** * Check if a code matches for a given secret and counter. */ diff --git a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java index ba85438..f66f0f9 100644 --- a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java +++ b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java @@ -3,6 +3,7 @@ import dev.samstevens.totp.exceptions.CodeGenerationException; import dev.samstevens.totp.time.TimeProvider; import org.junit.jupiter.api.Test; +import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -45,6 +46,23 @@ public void testCodeGenerationFailureReturnsFalse() throws CodeGenerationExcepti assertEquals(false, verifier.isValidCode(secret, "1234")); } + @Test + public void testConsecutiveCodesAreValid() { + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + String firstCode = "401619"; + String secondCode = "862707"; + String thirdCode = "927139"; + + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(1567975936L); + + DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); + verifier.setAllowedTimePeriodDuration(Duration.ofHours(24)); + + boolean result = verifier.areValidCodes(secret, firstCode, secondCode, thirdCode); + assertTrue(result); + } + private boolean isValidCode(String secret, String code, long time, int timePeriod) { TimeProvider timeProvider = mock(TimeProvider.class); when(timeProvider.getTime()).thenReturn(time); From 95246f503d7351ff48bbc6f2692f588713bd8c36 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 19 Apr 2020 15:10:01 +0100 Subject: [PATCH 02/14] design new API, initial implementation --- README.md | 14 ++++++++- totp-spring-boot-starter/README.md | 2 +- .../samstevens/totp/code/CodeVerifier.java | 14 +++++++++ .../totp/code/DefaultCodeVerifier.java | 29 ++++++++----------- .../samstevens/totp/code/VerifyResult.java | 19 ++++++++++++ .../totp/code/DefaultCodeVerifierTest.java | 11 +++---- 6 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java diff --git a/README.md b/README.md index f3f927d..932551c 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,23 @@ CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); // secret = the shared secret for the user // code = the code submitted by the user -boolean successful = verifier.isValidCode(secret, code) +VerifyResult result = verifier.verifyCode(secret, code); +boolean isValid = result.isValid(); +int timePeriodDifference = result.getTimePeriodDifference(); ``` This same process is used when verifying the submitted code every time the user needs to in the future. +#### Verifying multiple consecutive codes + +```java +// secret = the shared secret for the user +// code = the code submitted by the user +VerifyResult result = verifier.verifyConsecutiveCodes(secret, firstCode, secondCode, thirdCode...); +boolean isValid = result.isValid(); +int timePeriodDifference = result.getTimePeriodDifference(); +``` + #### Using different hashing algorithms By default, the `DefaultCodeGenerator` uses the SHA1 algorithm to generate/verify codes, but SHA256 and SHA512 are also supported. To use a different algorithm, pass in the desired `HashingAlgorithm` into the constructor: diff --git a/totp-spring-boot-starter/README.md b/totp-spring-boot-starter/README.md index a1c024f..a5beee3 100644 --- a/totp-spring-boot-starter/README.md +++ b/totp-spring-boot-starter/README.md @@ -83,7 +83,7 @@ public class MfaVerifyController { public String verify(@RequestParam String code) { // secret is fetched from some storage - if (verifier.isValidCode(secret, code)) { + if (verifier.verifyCode(secret, code).isValid()) { return "CORRECT CODE"; } diff --git a/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java index 711bc03..ed53c95 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java +++ b/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java @@ -2,9 +2,23 @@ public interface CodeVerifier { /** + * @deprecated Replaced by {@link #verifyCode(String, String)}. * @param secret The shared secret/key to check the code against. * @param code The n-digit code given by the end user to check. * @return If the code is valid or not. */ + @Deprecated boolean isValidCode(String secret, String code); + + /** + * @param secret The shared secret/key to check the code against. + * @param code The n-digit code given by the end user to check. + */ + VerifyResult verifyCode(String secret, String code); + + /** + * @param secret The shared secret/key to check the code against. + * @param codes The n-digit codes given by the end user to check. Codes must be valid and consecutive. + */ + VerifyResult verifyConsecutiveCodes(String secret, String... codes); } diff --git a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java index 39455cf..c53399f 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java +++ b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java @@ -34,33 +34,28 @@ public void setAllowedTimePeriodDuration(Duration duration) { } @Override + @Deprecated public boolean isValidCode(String secret, String code) { - // Get the current number of seconds since the epoch and - // calculate the number of time periods passed. - long currentBucket = Math.floorDiv(timeProvider.getTime(), timePeriod); - - // Calculate and compare the codes for all the "valid" time periods, - // even if we get an early match, to avoid timing attacks - boolean success = false; - for (int i = -allowedTimePeriodDiscrepancy; i <= allowedTimePeriodDiscrepancy; i++) { - success = checkCode(secret, currentBucket + i, code) || success; - } - - return success; + return verifyCode(secret, code).isValid(); } + @Override + public VerifyResult verifyCode(String secret, String code) { + return verifyConsecutiveCodes(secret, code); + } - public boolean areValidCodes(String secret, String... codes) { + @Override + public VerifyResult verifyConsecutiveCodes(String secret, String... codes) { // Get the current number of seconds since the epoch and // calculate the number of time periods passed. long currentBucket = Math.floorDiv(timeProvider.getTime(), timePeriod); // Calculate and compare the codes for all the "valid" time periods, // even if we get an early match, to avoid timing attacks + boolean success = false; - int successiveMatches = 0; int currentCodeBeingChecked = 0; - + int firstCodeMatchTimePeriod = 0; boolean isValid; for (int i = -allowedTimePeriodDiscrepancy; i <= allowedTimePeriodDiscrepancy; i++) { @@ -76,14 +71,14 @@ public boolean areValidCodes(String secret, String... codes) { } } - System.out.println(currentCodeBeingChecked); if (!success && currentCodeBeingChecked == codes.length) { success = true; currentCodeBeingChecked = 0; + firstCodeMatchTimePeriod = i - codes.length + 1; } } - return success; + return new VerifyResult(success, firstCodeMatchTimePeriod); } /** diff --git a/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java b/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java new file mode 100644 index 0000000..e0c0ffa --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java @@ -0,0 +1,19 @@ +package dev.samstevens.totp.code; + +public class VerifyResult { + private final boolean isValid; + private final int timePeriodDifference; + + public VerifyResult(boolean isValid, int timePeriodDifference) { + this.isValid = isValid; + this.timePeriodDifference = timePeriodDifference; + } + + public boolean isValid() { + return isValid; + } + + public int getTimePeriodDifference() { + return timePeriodDifference; + } +} diff --git a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java index f66f0f9..3aa2312 100644 --- a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java +++ b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java @@ -49,9 +49,9 @@ public void testCodeGenerationFailureReturnsFalse() throws CodeGenerationExcepti @Test public void testConsecutiveCodesAreValid() { String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; - String firstCode = "401619"; - String secondCode = "862707"; - String thirdCode = "927139"; + String firstCode = "092231"; + String secondCode = "517872"; + String thirdCode = "398601"; TimeProvider timeProvider = mock(TimeProvider.class); when(timeProvider.getTime()).thenReturn(1567975936L); @@ -59,8 +59,9 @@ public void testConsecutiveCodesAreValid() { DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); verifier.setAllowedTimePeriodDuration(Duration.ofHours(24)); - boolean result = verifier.areValidCodes(secret, firstCode, secondCode, thirdCode); - assertTrue(result); + VerifyResult r = verifier.verifyConsecutiveCodes(secret, firstCode, secondCode, thirdCode); + assertTrue(r.isValid()); + assertEquals(-2550, r.getTimePeriodDifference()); } private boolean isValidCode(String secret, String code, long time, int timePeriod) { From 6e4a9e0672b12eec6502aa51308daf2edc1e9aee Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Thu, 23 Apr 2020 23:35:49 +0100 Subject: [PATCH 03/14] add changelog to readme --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 932551c..5507aef 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,23 @@ To run the tests for the library with Maven, run `mvn test`. +## Changelog + +All notable changes to the project will be documented here. + +### v1.8 - 2020-04-24 +#### Added + +- New method to verify multiple consecutive codes. +- New method to set the time period discrepancy by supplying a time duration object. +- Abilty to get the time drift between user & server for valid codes. +- Changelog section to README. + +#### Changed + +- Deprecated `isValidCode` method in favour of new `verifyCode` method. + + ## License From f76a66ba4f99182d7d48965922993cef1f4d7c4e Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Thu, 23 Apr 2020 23:57:24 +0100 Subject: [PATCH 04/14] implement getTimeDrift method --- .../samstevens/totp/code/DefaultCodeVerifier.java | 7 ++++++- .../java/dev/samstevens/totp/code/VerifyResult.java | 13 ++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java index c53399f..ae91669 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java +++ b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java @@ -78,7 +78,7 @@ public VerifyResult verifyConsecutiveCodes(String secret, String... codes) { } } - return new VerifyResult(success, firstCodeMatchTimePeriod); + return new VerifyResult(success, getTimeDrift(firstCodeMatchTimePeriod)); } /** @@ -111,4 +111,9 @@ private boolean timeSafeStringComparison(String a, String b) { return result == 0; } + + private Duration getTimeDrift(int bucketDifferential) + { + return Duration.ofSeconds(bucketDifferential * timePeriod); + } } \ No newline at end of file diff --git a/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java b/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java index e0c0ffa..c6add5d 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java +++ b/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java @@ -1,19 +1,22 @@ package dev.samstevens.totp.code; +import java.time.Duration; + public class VerifyResult { private final boolean isValid; - private final int timePeriodDifference; + private final Duration timeDrift; - public VerifyResult(boolean isValid, int timePeriodDifference) { + public VerifyResult(boolean isValid, Duration timeDrift) { this.isValid = isValid; - this.timePeriodDifference = timePeriodDifference; + this.timeDrift = timeDrift; } public boolean isValid() { return isValid; } - public int getTimePeriodDifference() { - return timePeriodDifference; + public Duration getTimeDrift() + { + return timeDrift; } } From c81e071f94bad4d26fdc673168708a0da113dc98 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Fri, 24 Apr 2020 00:02:33 +0100 Subject: [PATCH 05/14] fix test --- .../java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java index 3aa2312..45e0703 100644 --- a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java +++ b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java @@ -61,7 +61,7 @@ public void testConsecutiveCodesAreValid() { VerifyResult r = verifier.verifyConsecutiveCodes(secret, firstCode, secondCode, thirdCode); assertTrue(r.isValid()); - assertEquals(-2550, r.getTimePeriodDifference()); + assertEquals(-76500, r.getTimeDrift().getSeconds()); } private boolean isValidCode(String secret, String code, long time, int timePeriod) { From 1f8dfb6f1cf4181e8f72914eb475ea81365d96a7 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sat, 25 Apr 2020 21:49:20 +0100 Subject: [PATCH 06/14] new implementation of CodeGenerator --- .../samstevens/totp/code/CodeGenerator.java | 33 ++++++- .../totp/code/DefaultCodeGenerator.java | 90 ++++++++++++++++++- .../samstevens/totp/code/GeneratedCode.java | 19 ++++ .../samstevens/totp/code/ValidityPeriod.java | 21 +++++ 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java create mode 100644 totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java diff --git a/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java index bf68424..bf9845e 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java @@ -1,13 +1,40 @@ package dev.samstevens.totp.code; import dev.samstevens.totp.exceptions.CodeGenerationException; +import java.time.Instant; +import java.util.List; public interface CodeGenerator { + + /** + * Returns a GeneratedCode object representing the code for a given secret and time. + * + * @param secret The shared secret/key to generate the code with. + * @param atTime An Instant object representing the time to generate the code for. + * @return The GeneratedCode object containing the n-digit code and validity period. + * @throws CodeGenerationException Thrown if the code generation fails for any reason. + */ + GeneratedCode generate(String secret, Instant atTime) throws CodeGenerationException; + + /** + * Returns a list of GeneratedCode objects representing the code for a given secret and time, + * and the N previous and next codes. + * + * @param secret The shared secret/key to generate the code with. + * @param atTime An Instant object representing the time to generate the code for. + * @return The list of GeneratedCode objects. + * @throws CodeGenerationException Thrown if the code generation fails for any reason. + */ + List generate(String secret, Instant atTime, int beforeAndAfter) throws CodeGenerationException; + /** + * Returns a list of GeneratedCode objects that are valid for a given secret between a start and end time. + * * @param secret The shared secret/key to generate the code with. - * @param counter The current time bucket number. Number of seconds since epoch / bucket period. - * @return The n-digit code for the secret/counter. + * @param startTime An Instant object representing the time to start generating codes. + * @param endTime An Instant object representing the time to stop generating codes. + * @return The list of GeneratedCode objects. * @throws CodeGenerationException Thrown if the code generation fails for any reason. */ - String generate(String secret, long counter) throws CodeGenerationException; + List generateAllBetween(String secret, Instant startTime, Instant endTime) throws CodeGenerationException; } diff --git a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java index e87a69e..dac0ea8 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java @@ -7,37 +7,109 @@ import java.security.InvalidKeyException; import java.security.InvalidParameterException; import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; public class DefaultCodeGenerator implements CodeGenerator { + private final static int DEFAULT_DIGITS_LENGTH = 6; + private final static Duration DEFAULT_VALIDITY_DURATION = Duration.ofSeconds(30); + private final HashingAlgorithm algorithm; private final int digits; + private final long validitySeconds; public DefaultCodeGenerator() { - this(HashingAlgorithm.SHA1, 6); + this(HashingAlgorithm.SHA1, DEFAULT_DIGITS_LENGTH, DEFAULT_VALIDITY_DURATION); } public DefaultCodeGenerator(HashingAlgorithm algorithm) { - this(algorithm, 6); + this(algorithm, DEFAULT_DIGITS_LENGTH, DEFAULT_VALIDITY_DURATION); } public DefaultCodeGenerator(HashingAlgorithm algorithm, int digits) { + this(algorithm, digits, DEFAULT_VALIDITY_DURATION); + } + + public DefaultCodeGenerator(HashingAlgorithm algorithm, int digits, Duration codeValidityDuration) { if (algorithm == null) { throw new InvalidParameterException("HashingAlgorithm must not be null."); } + if (digits < 1) { throw new InvalidParameterException("Number of digits must be higher than 0."); } + if (codeValidityDuration.getSeconds() < 1) { + throw new InvalidParameterException("Number of seconds codes are valid for must be at least 1."); + } + this.algorithm = algorithm; this.digits = digits; + this.validitySeconds = codeValidityDuration.getSeconds(); } @Override - public String generate(String key, long counter) throws CodeGenerationException { + public GeneratedCode generate(String key, Instant atTime) throws CodeGenerationException { + return generate(key, atTime, 0).get(0); + } + + @Override + public List generate(String key, Instant atTime, int howManyBeforeAndAfter) throws CodeGenerationException { + if (howManyBeforeAndAfter < 0) { + throw new InvalidParameterException("Number of codes before and after to generate must be greater or equal to zero."); + } + + long counter = getCounterForTime(atTime); + long startCounter = counter - howManyBeforeAndAfter; + long endCounter = counter + howManyBeforeAndAfter; + + return generateCodesForCounterRange(key, startCounter, endCounter); + } + + @Override + public List generateBetween(String key, Instant startTime, Instant endTime) throws CodeGenerationException { + if (endTime.isBefore(startTime)) { + throw new InvalidParameterException("End time must be after start time."); + } + + long startCounter = getCounterForTime(startTime); + long endCounter = getCounterForTime(endTime); + + return generateCodesForCounterRange(key, startCounter, endCounter); + } + + /** + * Get the counter value used to generate the hash for the given time. + */ + private long getCounterForTime(Instant time) { + return Math.floorDiv(time.getEpochSecond(), validitySeconds); + } + + /** + * Create the list of GeneratedCode objects for a given key & start/end counters. + */ + private List generateCodesForCounterRange(String key, long startCounter, long endCounter) throws CodeGenerationException { + List codes = new ArrayList<>(); + for (long i = startCounter; i <= endCounter; i++) { + codes.add(generateForCounter(key, i)); + } + + return codes; + } + + /** + * Create the GeneratedCode object for a given key & counter. + */ + private GeneratedCode generateForCounter(String key, long counter) throws CodeGenerationException { try { byte[] hash = generateHash(key, counter); - return getDigitsFromHash(hash); + String digits = getDigitsFromHash(hash); + ValidityPeriod validity = getValidityPeriodFromTime(counter); + + return new GeneratedCode(digits, validity); } catch (Exception e) { throw new CodeGenerationException("Failed to generate code. See nested exception.", e); } @@ -83,4 +155,14 @@ private String getDigitsFromHash(byte[] hash) { // Left pad with 0s for a n-digit code return String.format("%0" + digits + "d", truncatedHash); } + + /** + * Get the period of time (start and end Instants) that the code for a given counter is valid for. + */ + private ValidityPeriod getValidityPeriodFromTime(long counter) { + long startTimeSeconds = counter * validitySeconds; + long endTimeSeconds = startTimeSeconds + validitySeconds - 1; + + return new ValidityPeriod(Instant.ofEpochSecond(startTimeSeconds), Instant.ofEpochSecond(endTimeSeconds)); + } } diff --git a/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java b/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java new file mode 100644 index 0000000..943f3a3 --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java @@ -0,0 +1,19 @@ +package dev.samstevens.totp.code; + +public class GeneratedCode { + private final String digits; + private final ValidityPeriod validityPeriod; + + public GeneratedCode(String digits, ValidityPeriod validity) { + this.digits = digits; + this.validityPeriod = validity; + } + + public String getDigits() { + return digits; + } + + public ValidityPeriod getValidityPeriod() { + return validityPeriod; + } +} diff --git a/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java b/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java new file mode 100644 index 0000000..b742de3 --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java @@ -0,0 +1,21 @@ +package dev.samstevens.totp.code; + +import java.time.Instant; + +public class ValidityPeriod { + private final Instant start; + private final Instant end; + + public ValidityPeriod(Instant start, Instant end) { + this.start = start; + this.end = end; + } + + public Instant getStart() { + return start; + } + + public Instant getEnd() { + return end; + } +} From a7f3d31797378be15dc3edd40dbd73f12161433f Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sat, 25 Apr 2020 22:46:32 +0100 Subject: [PATCH 07/14] refactor TimeProviders to return Instant objects instead of epoch seconds --- .../main/java/dev/samstevens/totp/time/NtpTimeProvider.java | 5 +++-- .../java/dev/samstevens/totp/time/SystemTimeProvider.java | 4 ++-- .../src/main/java/dev/samstevens/totp/time/TimeProvider.java | 5 +++-- .../java/dev/samstevens/totp/time/NtpTimeProviderTest.java | 2 +- .../dev/samstevens/totp/time/SystemTimeProviderTest.java | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java b/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java index 078b21e..e06602b 100644 --- a/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java +++ b/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java @@ -5,6 +5,7 @@ import org.apache.commons.net.ntp.TimeInfo; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Instant; public class NtpTimeProvider implements TimeProvider { @@ -36,11 +37,11 @@ private NtpTimeProvider(String ntpHostname, int timeout, String dependentClass) } @Override - public long getTime() throws TimeProviderException { + public Instant getTime() throws TimeProviderException { try { TimeInfo timeInfo = client.getTime(ntpHost); - return (long) Math.floor(timeInfo.getReturnTime() / 1000L); + return Instant.ofEpochSecond((long) Math.floor(timeInfo.getReturnTime() / 1000L)); } catch (Exception e) { throw new TimeProviderException("Failed to provide time from NTP server. See nested exception.", e); } diff --git a/totp/src/main/java/dev/samstevens/totp/time/SystemTimeProvider.java b/totp/src/main/java/dev/samstevens/totp/time/SystemTimeProvider.java index 3d01f78..b06b4bd 100644 --- a/totp/src/main/java/dev/samstevens/totp/time/SystemTimeProvider.java +++ b/totp/src/main/java/dev/samstevens/totp/time/SystemTimeProvider.java @@ -5,7 +5,7 @@ public class SystemTimeProvider implements TimeProvider { @Override - public long getTime() throws TimeProviderException { - return Instant.now().getEpochSecond(); + public Instant getTime() throws TimeProviderException { + return Instant.now(); } } diff --git a/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java b/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java index 4cd0c93..fe621aa 100644 --- a/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java +++ b/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java @@ -1,10 +1,11 @@ package dev.samstevens.totp.time; import dev.samstevens.totp.exceptions.TimeProviderException; +import java.time.Instant; public interface TimeProvider { /** - * @return The number of seconds since Jan 1st 1970, 00:00:00 UTC. + * @return The current time as an Instant object. */ - long getTime() throws TimeProviderException; + Instant getTime() throws TimeProviderException; } diff --git a/totp/src/test/java/dev/samstevens/totp/time/NtpTimeProviderTest.java b/totp/src/test/java/dev/samstevens/totp/time/NtpTimeProviderTest.java index 730f8d9..daa978a 100644 --- a/totp/src/test/java/dev/samstevens/totp/time/NtpTimeProviderTest.java +++ b/totp/src/test/java/dev/samstevens/totp/time/NtpTimeProviderTest.java @@ -10,7 +10,7 @@ public class NtpTimeProviderTest { @Test public void testProvidesTime() throws UnknownHostException { TimeProvider time = new NtpTimeProvider("pool.ntp.org"); - long currentTime = time.getTime(); + long currentTime = time.getTime().getEpochSecond(); // epoch should be 10 digits for the foreseeable future... assertEquals(10, String.valueOf(currentTime).length()); diff --git a/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java b/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java index 397a935..58be66b 100644 --- a/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java +++ b/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java @@ -11,7 +11,7 @@ public void testProvidesTime() { long currentTime = Instant.now().getEpochSecond(); TimeProvider time = new SystemTimeProvider(); - long providedTime = time.getTime(); + long providedTime = time.getTime().getEpochSecond(); // allow +=5 second discrepancy for test environments assertTrue(currentTime - 5 <= providedTime); From e0c7e3de5f41a7b5be02074ada62709d5a56c8bf Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 16:00:32 +0100 Subject: [PATCH 08/14] more work on code generation refactor --- .../samstevens/totp/code/CodeGenerator.java | 12 +- .../totp/code/DefaultCodeGenerator.java | 5 - .../samstevens/totp/code/GeneratedCode.java | 16 ++ .../samstevens/totp/code/ValidityPeriod.java | 15 ++ .../totp/code/DefaultCodeGeneratorTest.java | 200 +++++++++++++++--- .../totp/code/GeneratedCodeTest.java | 36 ++++ .../totp/code/ValidityPeriodTest.java | 25 +++ 7 files changed, 262 insertions(+), 47 deletions(-) create mode 100644 totp/src/test/java/dev/samstevens/totp/code/GeneratedCodeTest.java create mode 100644 totp/src/test/java/dev/samstevens/totp/code/ValidityPeriodTest.java diff --git a/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java index bf9845e..7bd70d4 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java @@ -6,16 +6,6 @@ public interface CodeGenerator { - /** - * Returns a GeneratedCode object representing the code for a given secret and time. - * - * @param secret The shared secret/key to generate the code with. - * @param atTime An Instant object representing the time to generate the code for. - * @return The GeneratedCode object containing the n-digit code and validity period. - * @throws CodeGenerationException Thrown if the code generation fails for any reason. - */ - GeneratedCode generate(String secret, Instant atTime) throws CodeGenerationException; - /** * Returns a list of GeneratedCode objects representing the code for a given secret and time, * and the N previous and next codes. @@ -36,5 +26,5 @@ public interface CodeGenerator { * @return The list of GeneratedCode objects. * @throws CodeGenerationException Thrown if the code generation fails for any reason. */ - List generateAllBetween(String secret, Instant startTime, Instant endTime) throws CodeGenerationException; + List generateBetween(String secret, Instant startTime, Instant endTime) throws CodeGenerationException; } diff --git a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java index dac0ea8..c18a524 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeGenerator.java @@ -51,11 +51,6 @@ public DefaultCodeGenerator(HashingAlgorithm algorithm, int digits, Duration cod this.validitySeconds = codeValidityDuration.getSeconds(); } - @Override - public GeneratedCode generate(String key, Instant atTime) throws CodeGenerationException { - return generate(key, atTime, 0).get(0); - } - @Override public List generate(String key, Instant atTime, int howManyBeforeAndAfter) throws CodeGenerationException { if (howManyBeforeAndAfter < 0) { diff --git a/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java b/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java index 943f3a3..f33e972 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java +++ b/totp/src/main/java/dev/samstevens/totp/code/GeneratedCode.java @@ -1,5 +1,7 @@ package dev.samstevens.totp.code; +import java.util.Objects; + public class GeneratedCode { private final String digits; private final ValidityPeriod validityPeriod; @@ -16,4 +18,18 @@ public String getDigits() { public ValidityPeriod getValidityPeriod() { return validityPeriod; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeneratedCode that = (GeneratedCode) o; + + return Objects.equals(digits, that.digits) && Objects.equals(validityPeriod, that.validityPeriod); + } + + @Override + public int hashCode() { + return Objects.hash(digits, validityPeriod); + } } diff --git a/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java b/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java index b742de3..9f5897c 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java +++ b/totp/src/main/java/dev/samstevens/totp/code/ValidityPeriod.java @@ -1,6 +1,7 @@ package dev.samstevens.totp.code; import java.time.Instant; +import java.util.Objects; public class ValidityPeriod { private final Instant start; @@ -18,4 +19,18 @@ public Instant getStart() { public Instant getEnd() { return end; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ValidityPeriod that = (ValidityPeriod) o; + + return Objects.equals(start, that.start) && Objects.equals(end, that.end); + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } } diff --git a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeGeneratorTest.java b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeGeneratorTest.java index d37bb54..f1d93cf 100644 --- a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeGeneratorTest.java +++ b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeGeneratorTest.java @@ -6,20 +6,134 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.security.InvalidParameterException; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.params.provider.Arguments.arguments; public class DefaultCodeGeneratorTest { + @Test + public void testInvalidHashingAlgorithmThrowsException() { + assertThrows(InvalidParameterException.class, () -> { + new DefaultCodeGenerator(null); + }); + } + + @Test + public void testInvalidDigitLengthThrowsException() { + assertThrows(InvalidParameterException.class, () -> { + new DefaultCodeGenerator(HashingAlgorithm.SHA1, 0); + }); + } + + @Test + public void testInvalidValidityDurationThrowsException() { + assertThrows(InvalidParameterException.class, () -> { + new DefaultCodeGenerator(HashingAlgorithm.SHA1, 6, Duration.ofSeconds(0)); + }); + } + + @Test + public void testGeneratingWithNegativeBeforeAndAfterThrowsException() + { + InvalidParameterException e = assertThrows(InvalidParameterException.class, () -> { + CodeGenerator generator = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 6); + generator.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", Instant.now(), - 1); + }); + + assertEquals("Number of codes before and after to generate must be greater or equal to zero.", e.getMessage()); + } + + + @Test + public void testGeneratingBetweenWithBadTimesThrowsException() + { + InvalidParameterException e = assertThrows(InvalidParameterException.class, () -> { + CodeGenerator generator = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 6); + generator.generateBetween("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", Instant.now(), Instant.now().minusSeconds(60)); + }); + + assertEquals("End time must be after start time.", e.getMessage()); + } + @ParameterizedTest - @MethodSource("expectedCodesProvider") - public void testCodeIsGenerated(String secret, int time, HashingAlgorithm algorithm, String expectedCode) throws CodeGenerationException { - String code = generateCode(algorithm, secret, time); + @MethodSource("testGeneratingCodesProvider") + public void testGeneratingCodes(HashingAlgorithm algo, String secret, Instant timeToRunAt, int disc, List expectedCodes) throws CodeGenerationException { + CodeGenerator generator = new DefaultCodeGenerator(algo); + List generatedCodes = generator.generate(secret, timeToRunAt, disc); + assertEquals(generatedCodes, expectedCodes); + } + + static Stream testGeneratingCodesProvider() { + return Stream.of( + arguments( + // Hashing algorithm, + HashingAlgorithm.SHA1, + // Secret + "W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", + // Time to run for + Instant.ofEpochSecond(1587772860), + // Number of codes to generate before and after + 2, + // Expected codes + Arrays.asList( + createGeneratedCode("897990", 1587772800, 1587772829), + createGeneratedCode("987260", 1587772830, 1587772859), + createGeneratedCode("594169", 1587772860, 1587772889), + createGeneratedCode("896541", 1587772890, 1587772919), + createGeneratedCode("733574", 1587772920, 1587772949) + ) + ), + + arguments( + // Hashing algorithm, + HashingAlgorithm.SHA256, + // Secret + "makrzl2hict4ojeji2iah4kndmq6sgka", + // Time to run for + Instant.ofEpochSecond(1593607513), + // Number of codes to generate before and after + 1, + // Expected codes + Arrays.asList( + createGeneratedCode("773412", 1593607470, 1593607499), + createGeneratedCode("250378", 1593607500, 1593607529), + createGeneratedCode("967103", 1593607530, 1593607559) + ) + ), - assertEquals(expectedCode, code); + arguments( + // Hashing algorithm, + HashingAlgorithm.SHA512, + // Secret + "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB", + // Time to run for + Instant.ofEpochSecond(1608940785), + // Number of codes to generate before and after + 3, + // Expected codes + Arrays.asList( + createGeneratedCode("381260", 1608940680, 1608940709), + createGeneratedCode("546994", 1608940710, 1608940739), + createGeneratedCode("492946", 1608940740, 1608940769), + + createGeneratedCode("977130", 1608940770, 1608940799), + + createGeneratedCode("416247", 1608940800, 1608940829), + createGeneratedCode("610646", 1608940830, 1608940859), + createGeneratedCode("418802", 1608940860, 1608940889) + ) + ) + ); } + + + static Stream expectedCodesProvider() { return Stream.of( arguments("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", 1567631536, HashingAlgorithm.SHA1, "082371"), @@ -32,47 +146,71 @@ static Stream expectedCodesProvider() { ); } + @Test - public void testDigitLength() throws CodeGenerationException { - DefaultCodeGenerator g = new DefaultCodeGenerator(HashingAlgorithm.SHA1); - String code = g.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", 1567631536); - assertEquals(6, code.length()); + public void testGeneratingMultipleCodesBetweenStartAndEndTimes() throws CodeGenerationException { - g = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 8); - code = g.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", 1567631536); - assertEquals(8, code.length()); + String secret = "W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY"; + // 2020-04-25 00:00:00 UTC + Instant startTime = Instant.ofEpochSecond(1587772800); + // 2020-04-25 00:01:30 UTC + Instant endTime = Instant.ofEpochSecond(1587772890); - g = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 4); - code = g.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", 1567631536); - assertEquals(4, code.length()); - } + DefaultCodeGenerator generator = new DefaultCodeGenerator(HashingAlgorithm.SHA1); - @Test - public void testInvalidHashingAlgorithmThrowsException() { - assertThrows(InvalidParameterException.class, () -> { - new DefaultCodeGenerator(null, 6); - }); + List expectedCodes = Arrays.asList("897990", "987260", "594169", "896541"); + List expectedStartTimes = Arrays.asList( + Instant.ofEpochSecond(1587772800), + Instant.ofEpochSecond(1587772830), + Instant.ofEpochSecond(1587772860), + Instant.ofEpochSecond(1587772890) + ); + List expectedEndTimes = Arrays.asList( + Instant.ofEpochSecond(1587772829), + Instant.ofEpochSecond(1587772859), + Instant.ofEpochSecond(1587772889), + Instant.ofEpochSecond(1587772919) + ); + + List codes = generator.generateBetween(secret, startTime, endTime); + + for (int i = 0; i < codes.size(); i++) { + assertEquals(expectedCodes.get(i), codes.get(i).getDigits()); + assertEquals(expectedStartTimes.get(i), codes.get(i).getValidityPeriod().getStart()); + assertEquals(expectedEndTimes.get(i), codes.get(i).getValidityPeriod().getEnd()); + } } - + @Test - public void testInvalidDigitLengthThrowsException() { - assertThrows(InvalidParameterException.class, () -> { - new DefaultCodeGenerator(HashingAlgorithm.SHA1, 0); - }); + public void testDigitLength() throws CodeGenerationException { +// DefaultCodeGenerator g = new DefaultCodeGenerator(HashingAlgorithm.SHA1); +// GeneratedCode code = g.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", Instant.ofEpochSecond(1567631536)); +// assertEquals(6, code.getDigits().length()); +// +// g = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 8); +// code = g.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", Instant.ofEpochSecond(1567631536)); +// assertEquals(8, code.getDigits().length()); +// +// g = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 4); +// code = g.generate("W3C5B3WKR4AUKFVWYU2WNMYB756OAKWY", Instant.ofEpochSecond(1567631536)); +// assertEquals(4, code.getDigits().length()); } + + @Test - public void testInvalidKeyThrowsCodeGenerationException() throws CodeGenerationException { + public void testInvalidKeyThrowsCodeGenerationException() { CodeGenerationException e = assertThrows(CodeGenerationException.class, () -> { DefaultCodeGenerator g = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 4); - g.generate("1234", 1567631536); + g.generate("1234", Instant.ofEpochSecond(1567631536), 0); }); assertNotNull(e.getCause()); } - private String generateCode(HashingAlgorithm algorithm, String secret, int time) throws CodeGenerationException { - long currentBucket = Math.floorDiv(time, 30); - DefaultCodeGenerator g = new DefaultCodeGenerator(algorithm); - return g.generate(secret, currentBucket); + /** + * Helper method to construct a GeneratedCode object. + */ + private static GeneratedCode createGeneratedCode(String digits, long startTime, long endTime) { + return new GeneratedCode(digits, new ValidityPeriod(Instant.ofEpochSecond(startTime), Instant.ofEpochSecond(endTime))); } } diff --git a/totp/src/test/java/dev/samstevens/totp/code/GeneratedCodeTest.java b/totp/src/test/java/dev/samstevens/totp/code/GeneratedCodeTest.java new file mode 100644 index 0000000..623ace7 --- /dev/null +++ b/totp/src/test/java/dev/samstevens/totp/code/GeneratedCodeTest.java @@ -0,0 +1,36 @@ +package dev.samstevens.totp.code; + +import org.junit.jupiter.api.Test; +import java.time.Instant; +import static org.junit.jupiter.api.Assertions.*; + +public class GeneratedCodeTest { + + @Test + public void testEquality() { + GeneratedCode code1 = createGeneratedCode("123456", 1567631536, 1567631596); + GeneratedCode code2 = createGeneratedCode("123456", 1567631536, 1567631596); + GeneratedCode code3 = createGeneratedCode("123457", 1567631536, 1567631596); + GeneratedCode code4 = createGeneratedCode("123456", 1567631536, 1567631597); + GeneratedCode code6 = new GeneratedCode("123457", null); + + assertTrue(code1.equals(code1)); + assertTrue(code1.equals(code2)); + assertTrue(code2.equals(code1)); + + assertFalse(code1.equals(code3)); + assertFalse(code3.equals(code1)); + assertFalse(code2.equals(code4)); + assertFalse(code3.equals(code6)); + + assertFalse(code1.equals(new Object())); + assertFalse(code1.equals(null)); + } + + /** + * Helper method to construct a GeneratedCode object. + */ + private static GeneratedCode createGeneratedCode(String digits, long startTime, long endTime) { + return new GeneratedCode(digits, new ValidityPeriod(Instant.ofEpochSecond(startTime), Instant.ofEpochSecond(endTime))); + } +} diff --git a/totp/src/test/java/dev/samstevens/totp/code/ValidityPeriodTest.java b/totp/src/test/java/dev/samstevens/totp/code/ValidityPeriodTest.java new file mode 100644 index 0000000..3bacb53 --- /dev/null +++ b/totp/src/test/java/dev/samstevens/totp/code/ValidityPeriodTest.java @@ -0,0 +1,25 @@ +package dev.samstevens.totp.code; + +import org.junit.jupiter.api.Test; +import java.time.Instant; +import static org.junit.jupiter.api.Assertions.*; + +public class ValidityPeriodTest { + + @Test + public void testEquality() { + ValidityPeriod period1 = new ValidityPeriod(Instant.ofEpochSecond(1567631536), Instant.ofEpochSecond(1567631596)); + ValidityPeriod period2 = new ValidityPeriod(Instant.ofEpochSecond(1567631536), Instant.ofEpochSecond(1567631596)); + ValidityPeriod period3 = new ValidityPeriod(Instant.ofEpochSecond(1567642536), Instant.ofEpochSecond(1568642536)); + + assertTrue(period1.equals(period1)); + assertTrue(period1.equals(period2)); + assertTrue(period2.equals(period1)); + + assertFalse(period1.equals(period3)); + assertFalse(period3.equals(period1)); + + assertFalse(period1.equals(new Object())); + assertFalse(period1.equals(null)); + } +} From d586e1fbaa72ba66405dda8479a0089701549b8d Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 16:06:20 +0100 Subject: [PATCH 09/14] move verify classes into own subpackage --- .../samstevens/totp/code/CodeVerifier.java | 24 --- .../totp/code/DefaultCodeVerifier.java | 119 --------------- .../samstevens/totp/verify/CodeVerifier.java | 15 ++ .../totp/verify/DefaultCodeVerifier.java | 144 ++++++++++++++++++ .../totp/{code => verify}/VerifyResult.java | 2 +- .../totp/code/DefaultCodeVerifierTest.java | 76 --------- .../totp/verify/DefaultCodeVerifierTest.java | 130 ++++++++++++++++ 7 files changed, 290 insertions(+), 220 deletions(-) delete mode 100644 totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java delete mode 100644 totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java create mode 100644 totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java create mode 100644 totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java rename totp/src/main/java/dev/samstevens/totp/{code => verify}/VerifyResult.java (91%) delete mode 100644 totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java create mode 100644 totp/src/test/java/dev/samstevens/totp/verify/DefaultCodeVerifierTest.java diff --git a/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java deleted file mode 100644 index ed53c95..0000000 --- a/totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.samstevens.totp.code; - -public interface CodeVerifier { - /** - * @deprecated Replaced by {@link #verifyCode(String, String)}. - * @param secret The shared secret/key to check the code against. - * @param code The n-digit code given by the end user to check. - * @return If the code is valid or not. - */ - @Deprecated - boolean isValidCode(String secret, String code); - - /** - * @param secret The shared secret/key to check the code against. - * @param code The n-digit code given by the end user to check. - */ - VerifyResult verifyCode(String secret, String code); - - /** - * @param secret The shared secret/key to check the code against. - * @param codes The n-digit codes given by the end user to check. Codes must be valid and consecutive. - */ - VerifyResult verifyConsecutiveCodes(String secret, String... codes); -} diff --git a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java deleted file mode 100644 index ae91669..0000000 --- a/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java +++ /dev/null @@ -1,119 +0,0 @@ -package dev.samstevens.totp.code; - -import dev.samstevens.totp.exceptions.CodeGenerationException; -import dev.samstevens.totp.time.TimeProvider; -import java.time.Duration; - -public class DefaultCodeVerifier implements CodeVerifier { - - private final CodeGenerator codeGenerator; - private final TimeProvider timeProvider; - private int timePeriod = 30; - private int allowedTimePeriodDiscrepancy = 1; - - public DefaultCodeVerifier(CodeGenerator codeGenerator, TimeProvider timeProvider) { - this.codeGenerator = codeGenerator; - this.timeProvider = timeProvider; - } - - public void setTimePeriod(int timePeriod) { - this.timePeriod = timePeriod; - } - - public int getTimePeriod() { - return timePeriod; - } - - public void setAllowedTimePeriodDiscrepancy(int allowedTimePeriodDiscrepancy) { - this.allowedTimePeriodDiscrepancy = allowedTimePeriodDiscrepancy; - } - - public void setAllowedTimePeriodDuration(Duration duration) { - long periods = duration.getSeconds() / this.getTimePeriod(); - this.setAllowedTimePeriodDiscrepancy((int) periods); - } - - @Override - @Deprecated - public boolean isValidCode(String secret, String code) { - return verifyCode(secret, code).isValid(); - } - - @Override - public VerifyResult verifyCode(String secret, String code) { - return verifyConsecutiveCodes(secret, code); - } - - @Override - public VerifyResult verifyConsecutiveCodes(String secret, String... codes) { - // Get the current number of seconds since the epoch and - // calculate the number of time periods passed. - long currentBucket = Math.floorDiv(timeProvider.getTime(), timePeriod); - - // Calculate and compare the codes for all the "valid" time periods, - // even if we get an early match, to avoid timing attacks - - boolean success = false; - int currentCodeBeingChecked = 0; - int firstCodeMatchTimePeriod = 0; - - boolean isValid; - for (int i = -allowedTimePeriodDiscrepancy; i <= allowedTimePeriodDiscrepancy; i++) { - isValid = checkCode(secret, currentBucket + i, codes[currentCodeBeingChecked]); - if (isValid) { - currentCodeBeingChecked++; - } else { - if (currentCodeBeingChecked > 0) { - isValid = checkCode(secret, currentBucket + i, codes[0]); - if (isValid) { - currentCodeBeingChecked++; - } - } - } - - if (!success && currentCodeBeingChecked == codes.length) { - success = true; - currentCodeBeingChecked = 0; - firstCodeMatchTimePeriod = i - codes.length + 1; - } - } - - return new VerifyResult(success, getTimeDrift(firstCodeMatchTimePeriod)); - } - - /** - * Check if a code matches for a given secret and counter. - */ - private boolean checkCode(String secret, long counter, String code) { - try { - String actualCode = codeGenerator.generate(secret, counter); - return timeSafeStringComparison(actualCode, code); - } catch (CodeGenerationException e) { - return false; - } - } - - /** - * Compare two strings for equality without leaking timing information. - */ - private boolean timeSafeStringComparison(String a, String b) { - byte[] aBytes = a.getBytes(); - byte[] bBytes = b.getBytes(); - - if (aBytes.length != bBytes.length) { - return false; - } - - int result = 0; - for (int i = 0; i < aBytes.length; i++) { - result |= aBytes[i] ^ bBytes[i]; - } - - return result == 0; - } - - private Duration getTimeDrift(int bucketDifferential) - { - return Duration.ofSeconds(bucketDifferential * timePeriod); - } -} \ No newline at end of file diff --git a/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java new file mode 100644 index 0000000..5221df7 --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java @@ -0,0 +1,15 @@ +package dev.samstevens.totp.verify; + +import java.time.temporal.TemporalAmount; +import java.util.List; + +public interface CodeVerifier { + + VerifyResult verifyCode(String secret, String code); + + VerifyResult verifyCode(String secret, String code, TemporalAmount acceptableTimeDrift); + + VerifyResult verifyCode(String secret, String code, int acceptableCodeDrift); + + VerifyResult verifyConsecutiveCodes(String secret, List codes, TemporalAmount timeWindow); +} diff --git a/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java new file mode 100644 index 0000000..5749fbd --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java @@ -0,0 +1,144 @@ +package dev.samstevens.totp.verify; + +import dev.samstevens.totp.code.CodeGenerator; +import dev.samstevens.totp.code.GeneratedCode; +import dev.samstevens.totp.exceptions.CodeGenerationException; +import dev.samstevens.totp.time.TimeProvider; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.TemporalAmount; +import java.util.ArrayList; +import java.util.List; + +public class DefaultCodeVerifier implements CodeVerifier { + + private final CodeGenerator codeGenerator; + private final TimeProvider timeProvider; + + public DefaultCodeVerifier(CodeGenerator codeGenerator, TimeProvider timeProvider) { + this.codeGenerator = codeGenerator; + this.timeProvider = timeProvider; + } + + @Override + public VerifyResult verifyCode(String secret, String code) { + return verifyCode(secret, code, 0); + } + + @Override + public VerifyResult verifyCode(String secret, String code, TemporalAmount acceptableTimeDrift) { + return verifyConsecutiveCodes(secret, listOf(code), acceptableTimeDrift); + } + + @Override + public VerifyResult verifyConsecutiveCodes(String secret, List codes, TemporalAmount acceptableTimeDrift) { + + Instant currentTime = timeProvider.getTime(); + Instant startTime = currentTime.minus(acceptableTimeDrift); + Instant endTime = currentTime.plus(acceptableTimeDrift); + + List generatedCodes; + try { + generatedCodes = codeGenerator.generateBetween(secret, startTime, endTime); + } catch (CodeGenerationException e) { + return new VerifyResult(false, null); + } + + return checkCodes(codes, generatedCodes, currentTime); + } + + @Override + public VerifyResult verifyCode(String secret, String code, int acceptableCodeDrift) { + Instant currentTime = timeProvider.getTime(); + + // Generate all the valid codes + List generatedCodes; + try { + generatedCodes = codeGenerator.generate(secret, currentTime, acceptableCodeDrift); + } catch (CodeGenerationException e) { + return new VerifyResult(false, null); + } + + // Check the code against the valid codes + return checkCodes(listOf(code), generatedCodes, currentTime); + } + + + + + + + + + + private List listOf(T item) { + List items = new ArrayList<>(); + items.add(item); + + return items; + } + + + + private VerifyResult checkCodes(List codes, List generatedCodes, Instant currentTime) { + boolean success = false; + GeneratedCode successCode = null; + + int currentCodeBeingChecked = 0; + + // Loop through all the generated codes + for (GeneratedCode generatedCode : generatedCodes) { + + // Is the current code we're checking valid? + boolean isValid = timeSafeStringComparison(generatedCode.getDigits(), codes.get(currentCodeBeingChecked)); + + if (isValid) { + currentCodeBeingChecked++; + } else { + isValid = timeSafeStringComparison(generatedCode.getDigits(), codes.get(0)); + if (isValid) { + currentCodeBeingChecked++; + } + } + + if (isValid && currentCodeBeingChecked == 1) { + successCode = generatedCode; + } + + if (!success && currentCodeBeingChecked == codes.size()) { + success = true; + currentCodeBeingChecked = 0; + } + } + + return new VerifyResult(success, getTimeDrift(currentTime, successCode)); + } + + + /** + * Compare two strings for equality without leaking timing information. + */ + private boolean timeSafeStringComparison(String a, String b) { + byte[] aBytes = a.getBytes(); + byte[] bBytes = b.getBytes(); + + if (aBytes.length != bBytes.length) { + return false; + } + + int result = 0; + for (int i = 0; i < aBytes.length; i++) { + result |= aBytes[i] ^ bBytes[i]; + } + + return result == 0; + } + + private Duration getTimeDrift(Instant currentTime, GeneratedCode successCode) { + if (successCode == null) { + return null; + } + + return Duration.between(currentTime, successCode.getValidityPeriod().getStart()); + } +} \ No newline at end of file diff --git a/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java b/totp/src/main/java/dev/samstevens/totp/verify/VerifyResult.java similarity index 91% rename from totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java rename to totp/src/main/java/dev/samstevens/totp/verify/VerifyResult.java index c6add5d..149f99f 100644 --- a/totp/src/main/java/dev/samstevens/totp/code/VerifyResult.java +++ b/totp/src/main/java/dev/samstevens/totp/verify/VerifyResult.java @@ -1,4 +1,4 @@ -package dev.samstevens.totp.code; +package dev.samstevens.totp.verify; import java.time.Duration; diff --git a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java b/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java deleted file mode 100644 index 45e0703..0000000 --- a/totp/src/test/java/dev/samstevens/totp/code/DefaultCodeVerifierTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.samstevens.totp.code; - -import dev.samstevens.totp.exceptions.CodeGenerationException; -import dev.samstevens.totp.time.TimeProvider; -import org.junit.jupiter.api.Test; -import java.time.Duration; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -public class DefaultCodeVerifierTest { - - @Test - public void testCodeIsValid() { - String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; - long timeToRunAt = 1567975936; - String correctCode = "862707"; - int timePeriod = 30; - - // allow for a -/+ ~30 second discrepancy - assertTrue(isValidCode(secret, correctCode, timeToRunAt - timePeriod, timePeriod)); - assertTrue(isValidCode(secret, correctCode, timeToRunAt, timePeriod)); - assertTrue(isValidCode(secret, correctCode, timeToRunAt + timePeriod, timePeriod)); - - // but no more - assertFalse(isValidCode(secret, correctCode, timeToRunAt + timePeriod + 15, timePeriod)); - - // test wrong code fails - assertFalse(isValidCode(secret, "123", timeToRunAt, timePeriod)); - } - - @Test - public void testCodeGenerationFailureReturnsFalse() throws CodeGenerationException { - - String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; - - TimeProvider timeProvider = mock(TimeProvider.class); - when(timeProvider.getTime()).thenReturn(1567975936L); - - CodeGenerator codeGenerator = mock(CodeGenerator.class); - when(codeGenerator.generate(anyString(), anyLong())).thenThrow(new CodeGenerationException("Test", new RuntimeException())); - - DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); - verifier.setAllowedTimePeriodDiscrepancy(1); - - - assertEquals(false, verifier.isValidCode(secret, "1234")); - } - - @Test - public void testConsecutiveCodesAreValid() { - String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; - String firstCode = "092231"; - String secondCode = "517872"; - String thirdCode = "398601"; - - TimeProvider timeProvider = mock(TimeProvider.class); - when(timeProvider.getTime()).thenReturn(1567975936L); - - DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); - verifier.setAllowedTimePeriodDuration(Duration.ofHours(24)); - - VerifyResult r = verifier.verifyConsecutiveCodes(secret, firstCode, secondCode, thirdCode); - assertTrue(r.isValid()); - assertEquals(-76500, r.getTimeDrift().getSeconds()); - } - - private boolean isValidCode(String secret, String code, long time, int timePeriod) { - TimeProvider timeProvider = mock(TimeProvider.class); - when(timeProvider.getTime()).thenReturn(time); - - DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); - verifier.setTimePeriod(timePeriod); - - return verifier.isValidCode(secret, code); - } -} diff --git a/totp/src/test/java/dev/samstevens/totp/verify/DefaultCodeVerifierTest.java b/totp/src/test/java/dev/samstevens/totp/verify/DefaultCodeVerifierTest.java new file mode 100644 index 0000000..d8fb708 --- /dev/null +++ b/totp/src/test/java/dev/samstevens/totp/verify/DefaultCodeVerifierTest.java @@ -0,0 +1,130 @@ +package dev.samstevens.totp.code; + +import dev.samstevens.totp.exceptions.CodeGenerationException; +import dev.samstevens.totp.time.TimeProvider; +import dev.samstevens.totp.verify.DefaultCodeVerifier; +import dev.samstevens.totp.verify.VerifyResult; +import org.junit.jupiter.api.Test; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class DefaultCodeVerifierTest { + + @Test + public void testCodeIsValid() { + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + long timeToRunAt = 1567975936; + String correctCode = "862707"; + Duration timePeriod = Duration.ofSeconds(30); + + // allow for a -/+ ~30 second discrepancy + assertTrue(isValidCode(secret, correctCode, timeToRunAt - timePeriod.getSeconds(), timePeriod)); + assertTrue(isValidCode(secret, correctCode, timeToRunAt, timePeriod)); + assertTrue(isValidCode(secret, correctCode, timeToRunAt + timePeriod.getSeconds(), timePeriod)); + + // but no more + assertFalse(isValidCode(secret, correctCode, timeToRunAt + timePeriod.getSeconds() + 15, timePeriod)); + + // test wrong code fails + assertFalse(isValidCode(secret, "123", timeToRunAt, timePeriod)); + } + + @Test + public void testCodeGenerationFailureReturnsFalse() throws CodeGenerationException { + + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(Instant.ofEpochSecond(1567975936L)); + + CodeGenerator codeGenerator = mock(CodeGenerator.class); + when(codeGenerator.generate(anyString(), any(Instant.class), anyInt())) + .thenThrow(new CodeGenerationException("Test", new RuntimeException())); + + DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); + VerifyResult result = verifier.verifyCode(secret, "1234"); + + assertEquals(false, result.isValid()); + assertNull(result.getTimeDrift()); + } + + ///////////////////// + + @Test + public void testVerifySingleCode() { + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + + // Ran at 2020-05-08 22:35:45 UTC + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(Instant.ofEpochSecond(1588977345)); + + DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); + + VerifyResult r = verifier.verifyCode(secret, "237357"); + assertTrue(r.isValid()); + assertEquals(-15, r.getTimeDrift().getSeconds()); + } + + @Test + public void testVerifySingleCodeWithCodeDrifts() { + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + + // Ran at 2020-05-08 22:35:45 UTC + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(Instant.ofEpochSecond(1588977345)); + + DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); + + VerifyResult r = verifier.verifyCode(secret, "631654", 1); + assertTrue(r.isValid()); + assertEquals(-45, r.getTimeDrift().getSeconds()); + } + + @Test + public void testVerifySingleCodeWithTimeDrifts() { + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + + // Ran at 2020-05-08 22:35:45 UTC + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(Instant.ofEpochSecond(1588977345)); + + DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); + + VerifyResult r = verifier.verifyCode(secret, "877212", Duration.ofMinutes(3)); + assertTrue(r.isValid()); + assertEquals(-165, r.getTimeDrift().getSeconds()); + } + + @Test + public void testConsecutiveCodesAreValid() { + String secret = "EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB"; + String firstCode = "092231"; + String secondCode = "517872"; + String thirdCode = "398601"; + + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(Instant.ofEpochSecond(1567975936L)); + + DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); + + List codes = Arrays.asList(firstCode, secondCode, thirdCode); + VerifyResult r = verifier.verifyConsecutiveCodes(secret, codes, Duration.ofHours(24)); + assertTrue(r.isValid()); + assertEquals(-76500, r.getTimeDrift().getSeconds()); + } + + private boolean isValidCode(String secret, String code, long time, Duration timePeriod) { + TimeProvider timeProvider = mock(TimeProvider.class); + when(timeProvider.getTime()).thenReturn(Instant.ofEpochSecond(time)); + +// DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider, timePeriod); +// +// return verifier.isValidCode(secret, code); + return false; + } +} From 52ba2a57dc924a1c45aca6e81eda494681bc716f Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 17:25:42 +0100 Subject: [PATCH 10/14] change NtpTimeProvider to use Duration object instead of int for timeout --- .../dev/samstevens/totp/time/NtpTimeProvider.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java b/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java index e06602b..615c80e 100644 --- a/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java +++ b/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java @@ -5,34 +5,35 @@ import org.apache.commons.net.ntp.TimeInfo; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Duration; import java.time.Instant; public class NtpTimeProvider implements TimeProvider { + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(3); private final NTPUDPClient client; private final InetAddress ntpHost; public NtpTimeProvider(String ntpHostname) throws UnknownHostException { - // default timeout of 3 seconds - this(ntpHostname, 3000); + this(ntpHostname, DEFAULT_TIMEOUT); } - public NtpTimeProvider(String ntpHostname, int timeout) throws UnknownHostException { + public NtpTimeProvider(String ntpHostname, Duration timeout) throws UnknownHostException { this(ntpHostname, timeout, "org.apache.commons.net.ntp.NTPUDPClient"); } // Package-private, for tests only NtpTimeProvider(String ntpHostname, String dependentClass) throws UnknownHostException { // default timeout of 3 seconds - this(ntpHostname, 3000, dependentClass); + this(ntpHostname, DEFAULT_TIMEOUT, dependentClass); } - private NtpTimeProvider(String ntpHostname, int timeout, String dependentClass) throws UnknownHostException { + private NtpTimeProvider(String ntpHostname, Duration timeout, String dependentClass) throws UnknownHostException { // Check the optional commons-net dependency is on the classpath checkHasDependency(dependentClass); client = new NTPUDPClient(); - client.setDefaultTimeout(timeout); + client.setDefaultTimeout((int) timeout.toMillis()); ntpHost = InetAddress.getByName(ntpHostname); } From 545fef8361ea85da11e896ff8d8424d787b7da5b Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 22:38:04 +0100 Subject: [PATCH 11/14] tidy up of time subpackage --- .../dev/samstevens/totp/time/NtpTimeProvider.java | 3 +-- .../dev/samstevens/totp/time/TimeProvider.java | 2 +- .../totp/time/SystemTimeProviderTest.java | 14 ++++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java b/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java index 615c80e..434d1bb 100644 --- a/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java +++ b/totp/src/main/java/dev/samstevens/totp/time/NtpTimeProvider.java @@ -24,7 +24,6 @@ public NtpTimeProvider(String ntpHostname, Duration timeout) throws UnknownHostE // Package-private, for tests only NtpTimeProvider(String ntpHostname, String dependentClass) throws UnknownHostException { - // default timeout of 3 seconds this(ntpHostname, DEFAULT_TIMEOUT, dependentClass); } @@ -50,7 +49,7 @@ public Instant getTime() throws TimeProviderException { private void checkHasDependency(String dependentClass) { try { - Class ntpClientClass = Class.forName(dependentClass); + Class.forName(dependentClass); } catch (ClassNotFoundException e) { throw new RuntimeException("The Apache Commons Net library must be on the classpath to use the NtpTimeProvider."); } diff --git a/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java b/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java index fe621aa..4cdcab0 100644 --- a/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java +++ b/totp/src/main/java/dev/samstevens/totp/time/TimeProvider.java @@ -5,7 +5,7 @@ public interface TimeProvider { /** - * @return The current time as an Instant object. + * @return The current time represented as an Instant object. */ Instant getTime() throws TimeProviderException; } diff --git a/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java b/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java index 58be66b..63a436f 100644 --- a/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java +++ b/totp/src/test/java/dev/samstevens/totp/time/SystemTimeProviderTest.java @@ -1,6 +1,7 @@ package dev.samstevens.totp.time; import org.junit.jupiter.api.Test; +import java.time.Duration; import java.time.Instant; import static org.junit.jupiter.api.Assertions.*; @@ -9,15 +10,16 @@ public class SystemTimeProviderTest { @Test public void testProvidesTime() { - long currentTime = Instant.now().getEpochSecond(); + Instant currentTime = Instant.now(); + TimeProvider time = new SystemTimeProvider(); - long providedTime = time.getTime().getEpochSecond(); + Instant providedTime = time.getTime(); - // allow +=5 second discrepancy for test environments - assertTrue(currentTime - 5 <= providedTime); - assertTrue(providedTime <= currentTime + 5); + // allow +/-3 second discrepancy for test environments + Duration difference = Duration.between(currentTime, providedTime); + assertTrue(Math.abs(difference.getSeconds()) < 3); // epoch should be 10 digits for the foreseeable future... - assertEquals(10, String.valueOf(currentTime).length()); + assertEquals(10, String.valueOf(providedTime.getEpochSecond()).length()); } } From e51ef6c4b781158fba84f2f6af53d61c642f09a6 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 22:44:56 +0100 Subject: [PATCH 12/14] tidy RecoveryCodeGenerator and change version in TotpInfo --- .../java/dev/samstevens/totp/TotpInfo.java | 2 +- .../totp/recovery/RecoveryCodeGenerator.java | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/totp/src/main/java/dev/samstevens/totp/TotpInfo.java b/totp/src/main/java/dev/samstevens/totp/TotpInfo.java index c493b8b..fbd658d 100644 --- a/totp/src/main/java/dev/samstevens/totp/TotpInfo.java +++ b/totp/src/main/java/dev/samstevens/totp/TotpInfo.java @@ -1,5 +1,5 @@ package dev.samstevens.totp; public class TotpInfo { - public static String VERSION = "1.7"; + public static String VERSION = "2.0.0"; } diff --git a/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java index 3100dff..4617314 100644 --- a/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java @@ -5,16 +5,18 @@ import java.util.Arrays; import java.util.Random; +/** + * Recovery codes must reach a minimum entropy to be secure. + * code entropy = log( {characters-count} ^ {code-length} ) / log(2) + * the settings used below allows the code to reach an entropy of 82 bits: + * log(36^16) / log(2) == 82.7... + * + * Recovery code must be simple to read and enter by end user when needed: + * - generate a code composed of numbers and lower case characters from latin alphabet (36 possible characters) + * - split code in groups separated with dash for better readability, for example 4ckn-xspn-et8t-xgr0 + */ public class RecoveryCodeGenerator { - // Recovery code must reach a minimum entropy to be secured - // code entropy = log( {characters-count} ^ {code-length} ) / log(2) - // the settings used below allows the code to reach an entropy of 82 bits : - // log(36^16) / log(2) == 82.7... - - // Recovery code must be simple to read and enter by end user when needed : - // - generate a code composed of numbers and lower case characters from latin alphabet (36 possible characters) - // - split code in groups separated with dash for better readability, for example 4ckn-xspn-et8t-xgr0 private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); private static final int CODE_LENGTH = 16; private static final int GROUPS_NBR = 4; @@ -49,5 +51,4 @@ private String generateCode() { return code.toString(); } - } \ No newline at end of file From 5bb5771a4a2d7dc40b6b24d2f79b31d7c7fde0f9 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 23:09:48 +0100 Subject: [PATCH 13/14] work on DefaultCodeVerifier. --- .../samstevens/totp/verify/CodeVerifier.java | 6 +- .../totp/verify/DefaultCodeVerifier.java | 118 +++++------------- 2 files changed, 32 insertions(+), 92 deletions(-) diff --git a/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java index 5221df7..c3d751e 100644 --- a/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java +++ b/totp/src/main/java/dev/samstevens/totp/verify/CodeVerifier.java @@ -7,9 +7,9 @@ public interface CodeVerifier { VerifyResult verifyCode(String secret, String code); - VerifyResult verifyCode(String secret, String code, TemporalAmount acceptableTimeDrift); + VerifyResult verifyCode(String secret, String code, int allowedNumberOfCodesBeforeAndAfter); - VerifyResult verifyCode(String secret, String code, int acceptableCodeDrift); + VerifyResult verifyCode(String secret, String code, TemporalAmount acceptableTimeDrift); - VerifyResult verifyConsecutiveCodes(String secret, List codes, TemporalAmount timeWindow); + VerifyResult verifyConsecutiveCodes(String secret, List codes, TemporalAmount acceptableTimeDrift); } diff --git a/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java b/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java index 5749fbd..7e72ab3 100644 --- a/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java +++ b/totp/src/main/java/dev/samstevens/totp/verify/DefaultCodeVerifier.java @@ -7,8 +7,9 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.TemporalAmount; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public class DefaultCodeVerifier implements CodeVerifier { @@ -26,119 +27,58 @@ public VerifyResult verifyCode(String secret, String code) { } @Override - public VerifyResult verifyCode(String secret, String code, TemporalAmount acceptableTimeDrift) { - return verifyConsecutiveCodes(secret, listOf(code), acceptableTimeDrift); - } - - @Override - public VerifyResult verifyConsecutiveCodes(String secret, List codes, TemporalAmount acceptableTimeDrift) { - + public VerifyResult verifyCode(String secret, String code, int allowedNumberOfCodesBeforeAndAfter) { Instant currentTime = timeProvider.getTime(); - Instant startTime = currentTime.minus(acceptableTimeDrift); - Instant endTime = currentTime.plus(acceptableTimeDrift); + // Generate all the valid codes List generatedCodes; try { - generatedCodes = codeGenerator.generateBetween(secret, startTime, endTime); + generatedCodes = codeGenerator.generate(secret, currentTime, allowedNumberOfCodesBeforeAndAfter); } catch (CodeGenerationException e) { return new VerifyResult(false, null); } - return checkCodes(codes, generatedCodes, currentTime); + // Check the code against the valid codes + return verifyConsecutiveCodesAreValid(Collections.singletonList(code), generatedCodes, currentTime); } @Override - public VerifyResult verifyCode(String secret, String code, int acceptableCodeDrift) { + public VerifyResult verifyCode(String secret, String code, TemporalAmount acceptableTimeDrift) { + return verifyConsecutiveCodes(secret, Collections.singletonList(code), acceptableTimeDrift); + } + + @Override + public VerifyResult verifyConsecutiveCodes(String secret, List codes, TemporalAmount acceptableTimeDrift) { Instant currentTime = timeProvider.getTime(); + Instant startTime = currentTime.minus(acceptableTimeDrift); + Instant endTime = currentTime.plus(acceptableTimeDrift); - // Generate all the valid codes List generatedCodes; try { - generatedCodes = codeGenerator.generate(secret, currentTime, acceptableCodeDrift); + generatedCodes = codeGenerator.generateBetween(secret, startTime, endTime); } catch (CodeGenerationException e) { return new VerifyResult(false, null); } - // Check the code against the valid codes - return checkCodes(listOf(code), generatedCodes, currentTime); - } - - - - - - - - - - private List listOf(T item) { - List items = new ArrayList<>(); - items.add(item); - - return items; - } - - - - private VerifyResult checkCodes(List codes, List generatedCodes, Instant currentTime) { - boolean success = false; - GeneratedCode successCode = null; - - int currentCodeBeingChecked = 0; - - // Loop through all the generated codes - for (GeneratedCode generatedCode : generatedCodes) { - - // Is the current code we're checking valid? - boolean isValid = timeSafeStringComparison(generatedCode.getDigits(), codes.get(currentCodeBeingChecked)); - - if (isValid) { - currentCodeBeingChecked++; - } else { - isValid = timeSafeStringComparison(generatedCode.getDigits(), codes.get(0)); - if (isValid) { - currentCodeBeingChecked++; - } - } - - if (isValid && currentCodeBeingChecked == 1) { - successCode = generatedCode; - } - - if (!success && currentCodeBeingChecked == codes.size()) { - success = true; - currentCodeBeingChecked = 0; - } - } - - return new VerifyResult(success, getTimeDrift(currentTime, successCode)); + return verifyConsecutiveCodesAreValid(codes, generatedCodes, currentTime); } + private VerifyResult verifyConsecutiveCodesAreValid(List codes, List generatedCodes, Instant currentTime) { + // Transform the list of GeneratedCodes into a list of just the digits + List generatedCodesStrings = generatedCodes.stream().map(GeneratedCode::getDigits).collect(Collectors.toList()); - /** - * Compare two strings for equality without leaking timing information. - */ - private boolean timeSafeStringComparison(String a, String b) { - byte[] aBytes = a.getBytes(); - byte[] bBytes = b.getBytes(); + // Check if the given codes exist consecutively in the generated codes + int firstValidCodeIndex = Collections.indexOfSubList(generatedCodesStrings, codes); - if (aBytes.length != bBytes.length) { - return false; - } - - int result = 0; - for (int i = 0; i < aBytes.length; i++) { - result |= aBytes[i] ^ bBytes[i]; + // Given codes do not consecutively appear in valid codes + if (firstValidCodeIndex < 0) { + return new VerifyResult(false, null); } - return result == 0; - } - - private Duration getTimeDrift(Instant currentTime, GeneratedCode successCode) { - if (successCode == null) { - return null; - } + // Codes do appear consecutively, get the duration between now and the first code being valid + GeneratedCode firstCode = generatedCodes.get(firstValidCodeIndex); + Duration timeDrift = Duration.between(currentTime, firstCode.getValidityPeriod().getStart()); - return Duration.between(currentTime, successCode.getValidityPeriod().getStart()); + return new VerifyResult(true, timeDrift); } } \ No newline at end of file From 1b87ecdc487198d04a61549007685af694b6d5d4 Mon Sep 17 00:00:00 2001 From: Sam Stevens Date: Sun, 26 Apr 2020 23:24:39 +0100 Subject: [PATCH 14/14] make required API changes to QR subpackage and spring-boot-starter module --- .../autoconfigure/TotpAutoConfiguration.java | 17 +++++------ .../spring/autoconfigure/TotpProperties.java | 30 ++++--------------- .../java/dev/samstevens/totp/qr/QrData.java | 13 ++++---- .../dev/samstevens/totp/qr/QrDataFactory.java | 5 ++-- .../samstevens/totp/qr/QrDataFactoryTest.java | 5 ++-- .../dev/samstevens/totp/qr/QrDataTest.java | 5 ++-- .../totp/qr/ZxingPngQrGeneratorTest.java | 5 ++-- 7 files changed, 33 insertions(+), 47 deletions(-) diff --git a/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpAutoConfiguration.java b/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpAutoConfiguration.java index daf8d19..0018d4e 100644 --- a/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpAutoConfiguration.java +++ b/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpAutoConfiguration.java @@ -10,12 +10,15 @@ import dev.samstevens.totp.secret.SecretGenerator; import dev.samstevens.totp.time.SystemTimeProvider; import dev.samstevens.totp.time.TimeProvider; +import dev.samstevens.totp.verify.CodeVerifier; +import dev.samstevens.totp.verify.DefaultCodeVerifier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.time.Duration; @Configuration @ConditionalOnClass(TotpInfo.class) @@ -51,7 +54,7 @@ public HashingAlgorithm hashingAlgorithm() { @Bean @ConditionalOnMissingBean public QrDataFactory qrDataFactory(HashingAlgorithm hashingAlgorithm) { - return new QrDataFactory(hashingAlgorithm, getCodeLength(), getTimePeriod()); + return new QrDataFactory(hashingAlgorithm, getCodeLength(), getCodeValidityDuration()); } @Bean @@ -63,17 +66,13 @@ public QrGenerator qrGenerator() { @Bean @ConditionalOnMissingBean public CodeGenerator codeGenerator(HashingAlgorithm algorithm) { - return new DefaultCodeGenerator(algorithm, getCodeLength()); + return new DefaultCodeGenerator(algorithm, getCodeLength(), getCodeValidityDuration()); } @Bean @ConditionalOnMissingBean public CodeVerifier codeVerifier(CodeGenerator codeGenerator, TimeProvider timeProvider) { - DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); - verifier.setTimePeriod(getTimePeriod()); - verifier.setAllowedTimePeriodDiscrepancy(props.getTime().getDiscrepancy()); - - return verifier; + return new DefaultCodeVerifier(codeGenerator, timeProvider); } @Bean @@ -86,7 +85,7 @@ private int getCodeLength() { return props.getCode().getLength(); } - private int getTimePeriod() { - return props.getTime().getPeriod(); + private Duration getCodeValidityDuration() { + return Duration.ofSeconds(props.getCode().getValiditySeconds()); } } diff --git a/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpProperties.java b/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpProperties.java index 3d1ea3a..8f14032 100644 --- a/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpProperties.java +++ b/totp-spring-boot-starter/src/main/java/dev/samstevens/totp/spring/autoconfigure/TotpProperties.java @@ -7,12 +7,10 @@ public class TotpProperties { private static final int DEFAULT_SECRET_LENGTH = 32; private static final int DEFAULT_CODE_LENGTH = 6; - private static final int DEFAULT_TIME_PERIOD = 30; - private static final int DEFAULT_TIME_DISCREPANCY = 1; + private static final int DEFAULT_CODE_VALIDITY_SECONDS = 30; private final Secret secret = new Secret(); private final Code code = new Code(); - private final Time time = new Time(); public Secret getSecret() { return secret; @@ -22,10 +20,6 @@ public Code getCode() { return code; } - public Time getTime() { - return time; - } - public static class Secret { private int length = DEFAULT_SECRET_LENGTH; @@ -40,6 +34,7 @@ public void setLength(int length) { public static class Code { private int length = DEFAULT_CODE_LENGTH; + private int validitySeconds = DEFAULT_CODE_VALIDITY_SECONDS; public int getLength() { return length; @@ -48,26 +43,13 @@ public int getLength() { public void setLength(int length) { this.length = length; } - } - - public static class Time { - private int period = DEFAULT_TIME_PERIOD; - private int discrepancy = DEFAULT_TIME_DISCREPANCY; - - public int getPeriod() { - return period; - } - - public void setPeriod(int period) { - this.period = period; - } - public int getDiscrepancy() { - return discrepancy; + public int getValiditySeconds() { + return validitySeconds; } - public void setDiscrepancy(int discrepancy) { - this.discrepancy = discrepancy; + public void setValiditySeconds(int validitySeconds) { + this.validitySeconds = validitySeconds; } } } diff --git a/totp/src/main/java/dev/samstevens/totp/qr/QrData.java b/totp/src/main/java/dev/samstevens/totp/qr/QrData.java index 7086e2a..3a27878 100644 --- a/totp/src/main/java/dev/samstevens/totp/qr/QrData.java +++ b/totp/src/main/java/dev/samstevens/totp/qr/QrData.java @@ -5,6 +5,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Duration; @SuppressWarnings("WeakerAccess") public class QrData { @@ -15,12 +16,12 @@ public class QrData { private final String issuer; private final String algorithm; private final int digits; - private final int period; + private final Duration period; /** * Force use of builder to create instances. */ - private QrData(String type, String label, String secret, String issuer, String algorithm, int digits, int period) { + private QrData(String type, String label, String secret, String issuer, String algorithm, int digits, Duration period) { this.type = type; this.label = label; this.secret = secret; @@ -54,7 +55,7 @@ public int getDigits() { return digits; } - public int getPeriod() { + public Duration getPeriod() { return period; } @@ -70,7 +71,7 @@ public String getUri() { "&issuer=" + uriEncode(issuer) + "&algorithm=" + uriEncode(algorithm) + "&digits=" + digits + - "&period=" + period; + "&period=" + period.getSeconds(); } private String uriEncode(String text) { @@ -93,7 +94,7 @@ public static class Builder { private String issuer; private HashingAlgorithm algorithm = HashingAlgorithm.SHA1; private int digits = 6; - private int period = 30; + private Duration period = Duration.ofSeconds(30); public Builder label(String label) { this.label = label; @@ -120,7 +121,7 @@ public Builder digits(int digits) { return this; } - public Builder period(int period) { + public Builder period(Duration period) { this.period = period; return this; } diff --git a/totp/src/main/java/dev/samstevens/totp/qr/QrDataFactory.java b/totp/src/main/java/dev/samstevens/totp/qr/QrDataFactory.java index 2932ae5..fb6336b 100644 --- a/totp/src/main/java/dev/samstevens/totp/qr/QrDataFactory.java +++ b/totp/src/main/java/dev/samstevens/totp/qr/QrDataFactory.java @@ -1,14 +1,15 @@ package dev.samstevens.totp.qr; import dev.samstevens.totp.code.HashingAlgorithm; +import java.time.Duration; public class QrDataFactory { private HashingAlgorithm defaultAlgorithm; private int defaultDigits; - private int defaultTimePeriod; + private Duration defaultTimePeriod; - public QrDataFactory(HashingAlgorithm defaultAlgorithm, int defaultDigits, int defaultTimePeriod) { + public QrDataFactory(HashingAlgorithm defaultAlgorithm, int defaultDigits, Duration defaultTimePeriod) { this.defaultAlgorithm = defaultAlgorithm; this.defaultDigits = defaultDigits; this.defaultTimePeriod = defaultTimePeriod; diff --git a/totp/src/test/java/dev/samstevens/totp/qr/QrDataFactoryTest.java b/totp/src/test/java/dev/samstevens/totp/qr/QrDataFactoryTest.java index ccc6d64..e7e9f4e 100644 --- a/totp/src/test/java/dev/samstevens/totp/qr/QrDataFactoryTest.java +++ b/totp/src/test/java/dev/samstevens/totp/qr/QrDataFactoryTest.java @@ -2,6 +2,7 @@ import dev.samstevens.totp.code.HashingAlgorithm; import org.junit.jupiter.api.Test; +import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; public class QrDataFactoryTest { @@ -9,11 +10,11 @@ public class QrDataFactoryTest { @Test public void testFactorySetsDefaultsOnBuilder() { - QrDataFactory qrDataFactory = new QrDataFactory(HashingAlgorithm.SHA256, 6, 30); + QrDataFactory qrDataFactory = new QrDataFactory(HashingAlgorithm.SHA256, 6, Duration.ofSeconds(30)); QrData data = qrDataFactory.newBuilder().build(); assertEquals(HashingAlgorithm.SHA256.getFriendlyName(), data.getAlgorithm()); assertEquals(6, data.getDigits()); - assertEquals(30, data.getPeriod()); + assertEquals(Duration.ofSeconds(30), data.getPeriod()); } } diff --git a/totp/src/test/java/dev/samstevens/totp/qr/QrDataTest.java b/totp/src/test/java/dev/samstevens/totp/qr/QrDataTest.java index 6da428b..8c860d0 100644 --- a/totp/src/test/java/dev/samstevens/totp/qr/QrDataTest.java +++ b/totp/src/test/java/dev/samstevens/totp/qr/QrDataTest.java @@ -2,6 +2,7 @@ import dev.samstevens.totp.code.HashingAlgorithm; import org.junit.jupiter.api.Test; +import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; public class QrDataTest { @@ -14,7 +15,7 @@ public void testUriGeneration() { .issuer("AppName AppCorp") .algorithm(HashingAlgorithm.SHA256) .digits(6) - .period(30) + .period(Duration.ofSeconds(30)) .build(); assertEquals( @@ -31,7 +32,7 @@ public void testNullFieldUriGeneration() { .issuer("AppName AppCorp") .algorithm(HashingAlgorithm.SHA256) .digits(6) - .period(30) + .period(Duration.ofSeconds(30)) .build(); assertEquals( diff --git a/totp/src/test/java/dev/samstevens/totp/qr/ZxingPngQrGeneratorTest.java b/totp/src/test/java/dev/samstevens/totp/qr/ZxingPngQrGeneratorTest.java index 88dc5d0..2b2450d 100644 --- a/totp/src/test/java/dev/samstevens/totp/qr/ZxingPngQrGeneratorTest.java +++ b/totp/src/test/java/dev/samstevens/totp/qr/ZxingPngQrGeneratorTest.java @@ -8,6 +8,7 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.time.Duration; import static dev.samstevens.totp.IOUtils.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -25,7 +26,7 @@ public void testSomething() throws QrGenerationException, IOException { .secret("EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB") .issuer("AppName") .digits(6) - .period(30) + .period(Duration.ofSeconds(30)) .build(); writeFile(generator.generate(data), "./test_qr.png"); @@ -78,7 +79,7 @@ private QrData getData() { .secret("EX47GINFPBK5GNLYLILGD2H6ZLGJNNWB") .issuer("AppName") .digits(6) - .period(30) + .period(Duration.ofSeconds(30)) .build(); } }