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

Fix #489: Make possible to configure time step length of TOTP #490

Merged
merged 1 commit into from
Jul 25, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;

Expand All @@ -40,7 +41,7 @@ public final class Totp {
/**
* Default time-step size of 30 seconds recommended by RFC. The value is selected as a balance between security and usability.
*/
private static final int TIME_STEP_X = 30;
private static final Duration DEFAULT_STEP_LENGTH = Duration.ofSeconds(30);

private Totp() {
throw new IllegalStateException("Should not be instantiated");
Expand All @@ -60,7 +61,21 @@ private Totp() {
* @throws CryptoProviderException in case of any crypto error
*/
public static byte[] generateTotpSha256(final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
return generateTotp(key, countTimeSteps(instant), digitsNumber, Algorithm.HMAC_SHA256.code);
return generateTotpSha256(key, instant, DEFAULT_STEP_LENGTH, digitsNumber);
}

/**
* Generates a TOTP value for the given set of parameters using HmacSHA256 algorithm.
*
* @param key the shared secret
* @param instant current Unix time
* @param stepLength length of time step
* @param digitsNumber number of digits to return
* @return a numeric String in base 10 that includes truncation digits
* @throws CryptoProviderException in case of any crypto error
*/
public static byte[] generateTotpSha256(final byte[] key, final Instant instant, final Duration stepLength, final int digitsNumber) throws CryptoProviderException {
return generateTotp(key, countTimeSteps(instant, stepLength), digitsNumber, Algorithm.HMAC_SHA256.code);
}

/**
Expand All @@ -73,7 +88,21 @@ public static byte[] generateTotpSha256(final byte[] key, final Instant instant,
* @throws CryptoProviderException in case of any crypto error
*/
public static byte[] generateTotpSha512(final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
return generateTotp(key, countTimeSteps(instant), digitsNumber, Algorithm.HMAC_SHA512.code);
return generateTotpSha512(key, instant, DEFAULT_STEP_LENGTH, digitsNumber);
}

/**
* Generates a TOTP value for the given set of parameters using HmacSHA512 algorithm.
*
* @param key the shared secret
* @param instant current Unix time
* @param stepLength length of time step
* @param digitsNumber number of digits to return
* @return a numeric String in base 10 that includes truncation digits
* @throws CryptoProviderException in case of any crypto error
*/
public static byte[] generateTotpSha512(final byte[] key, final Instant instant, final Duration stepLength, final int digitsNumber) throws CryptoProviderException {
return generateTotp(key, countTimeSteps(instant, stepLength), digitsNumber, Algorithm.HMAC_SHA512.code);
}

/**
Expand All @@ -85,10 +114,10 @@ public static byte[] generateTotpSha512(final byte[] key, final Instant instant,
* @param digitsNumber expected length of the TOTP
* @return true if OTP is valid
* @throws CryptoProviderException in case of any crypto error
* @see #validateTotpSha256(byte[], byte[], Instant, int)
* @see #validateTotpSha256(byte[], byte[], Instant, int, int, Duration)
*/
public static boolean validateTotpSha256(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
return validateTotpSha256(otp, key, instant, digitsNumber, 1);
return validateTotpSha256(otp, key, instant, digitsNumber, 1, DEFAULT_STEP_LENGTH);
}

/**
Expand All @@ -99,11 +128,12 @@ public static boolean validateTotpSha256(final byte[] otp, final byte[] key, fin
* @param instant current Unix time
* @param digitsNumber expected length of the TOTP
* @param steps number of backward time steps allowed to validate
* @param stepLength length of time step
* @return true if OTP is valid
* @throws CryptoProviderException in case of any crypto error
*/
public static boolean validateTotpSha256(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int steps) throws CryptoProviderException {
return validateTotp(otp, key, instant, digitsNumber, steps, Algorithm.HMAC_SHA256.code);
public static boolean validateTotpSha256(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int steps, final Duration stepLength) throws CryptoProviderException {
return validateTotp(otp, key, instant, digitsNumber, steps, stepLength, Algorithm.HMAC_SHA256.code);
}

/**
Expand All @@ -115,10 +145,10 @@ public static boolean validateTotpSha256(final byte[] otp, final byte[] key, fin
* @param digitsNumber expected length of the TOTP
* @return true if OTP is valid
* @throws CryptoProviderException in case of any crypto error
* @see #validateTotpSha512(byte[], byte[], Instant, int, int)
* @see #validateTotpSha512(byte[], byte[], Instant, int, int, Duration)
*/
public static boolean validateTotpSha512(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
return validateTotpSha512(otp, key, instant, digitsNumber, 1);
return validateTotpSha512(otp, key, instant, digitsNumber, 1, DEFAULT_STEP_LENGTH);
}

/**
Expand All @@ -129,11 +159,12 @@ public static boolean validateTotpSha512(final byte[] otp, final byte[] key, fin
* @param instant current Unix time
* @param digitsNumber expected length of the TOTP
* @param steps number of backward time steps allowed to validate
* @param stepLength length of time step
* @return true if OTP is valid
* @throws CryptoProviderException in case of any crypto error
*/
public static boolean validateTotpSha512(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int steps) throws CryptoProviderException {
return validateTotp(otp, key, instant, digitsNumber, steps, Algorithm.HMAC_SHA512.code);
public static boolean validateTotpSha512(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int steps, final Duration stepLength) throws CryptoProviderException {
return validateTotp(otp, key, instant, digitsNumber, steps, stepLength, Algorithm.HMAC_SHA512.code);
}

/**
Expand All @@ -144,12 +175,13 @@ public static boolean validateTotpSha512(final byte[] otp, final byte[] key, fin
* @param instant current Unix time
* @param digitsNumber expected length of the TOTP
* @param backwardSteps number of backward time steps allowed to validate
* @param stepLength length of time step
* @param algorithm the algorithm to use
* @return true if OTP is valid
* @throws CryptoProviderException in case of any crypto error
*/
private static boolean validateTotp(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int backwardSteps, final String algorithm) throws CryptoProviderException {
logger.debug("Validating TOTP for instant={}, algorithm={}, steps={}", instant, algorithm, backwardSteps);
private static boolean validateTotp(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int backwardSteps, final Duration stepLength, final String algorithm) throws CryptoProviderException {
logger.debug("Validating TOTP for instant={}, algorithm={}, steps={}, stepLength={}", instant, algorithm, backwardSteps, stepLength);

if (otp == null) {
throw new CryptoProviderException("Otp is mandatory");
Expand All @@ -163,7 +195,7 @@ private static boolean validateTotp(final byte[] otp, final byte[] key, final In
throw new CryptoProviderException("Steps must not be negative number");
}

final long currentTimeStep = countTimeSteps(instant);
final long currentTimeStep = countTimeSteps(instant, stepLength);
for (int i = 0; i <= backwardSteps; i++) {
logger.debug("Validating TOTP for instant={}, algorithm={}, step={} out of allowed backward steps={}", instant, algorithm, i, backwardSteps);
final long step = currentTimeStep - i;
Expand Down Expand Up @@ -224,12 +256,15 @@ private static byte[] generateTotp(final byte[] key, final long timeStep, final
return padWithZeros(Integer.toString(otp), digitsNumber).getBytes();
}

private static long countTimeSteps(final Instant instant) throws CryptoProviderException {
private static long countTimeSteps(final Instant instant, final Duration stepLength) throws CryptoProviderException {
if (instant == null) {
throw new CryptoProviderException("Instant is mandatory");
}
if (stepLength == null) {
throw new CryptoProviderException("StepLength is mandatory");
}

return instant.getEpochSecond() / TIME_STEP_X;
return instant.getEpochSecond() / stepLength.getSeconds();
}

private static String padWithZeros(final String source, final int length) {
Expand Down