Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Verify multiple consecutive codes #35

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -262,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

Expand Down
2 changes: 1 addition & 1 deletion totp-spring-boot-starter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,10 +20,6 @@ public Code getCode() {
return code;
}

public Time getTime() {
return time;
}

public static class Secret {
private int length = DEFAULT_SECRET_LENGTH;

Expand All @@ -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;
Expand All @@ -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;
}
}
}
2 changes: 1 addition & 1 deletion totp/src/main/java/dev/samstevens/totp/TotpInfo.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package dev.samstevens.totp;

public class TotpInfo {
public static String VERSION = "1.7";
public static String VERSION = "2.0.0";
}
23 changes: 20 additions & 3 deletions totp/src/main/java/dev/samstevens/totp/code/CodeGenerator.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
package dev.samstevens.totp.code;

import dev.samstevens.totp.exceptions.CodeGenerationException;
import java.time.Instant;
import java.util.List;

public interface CodeGenerator {

/**
* 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<GeneratedCode> 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<GeneratedCode> generateBetween(String secret, Instant startTime, Instant endTime) throws CodeGenerationException;
}
10 changes: 0 additions & 10 deletions totp/src/main/java/dev/samstevens/totp/code/CodeVerifier.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,104 @@
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 List<GeneratedCode> 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 String generate(String key, long counter) throws CodeGenerationException {
public List<GeneratedCode> 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<GeneratedCode> generateCodesForCounterRange(String key, long startCounter, long endCounter) throws CodeGenerationException {
List<GeneratedCode> 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);
}
Expand Down Expand Up @@ -83,4 +150,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));
}
}
Loading