BastiaanJansen / otp-java

A small and easy-to-use one-time password generator library for Java implementing RFC 4226 (HOTP) and RFC 6238 (TOTP).
MIT License
186 stars 30 forks source link

Does not generate same OTP for same secret and time duration #61

Closed davija closed 2 years ago

davija commented 2 years ago

Java Version: OpenJDK 15 OS: Ubuntu Linux

For the application I am working with, I need to be able to generate the same code for a 15 minute interval. However, this library seems to only allow a max time of 5 minutes before it changes the code.

Here is the code I am using

    @Autowired
    private Cache<String, byte[]> otpKeyCache;

    @Override
    public String generate(String username, String emailAddress)
    {
        var otpGenerator = getOtpGenerator(emailAddress);
        var now = Instant.now();
        var otp = otpGenerator.now();

        logger.info(String.format("Time Step is: %d", otpGenerator.getPeriod().getSeconds()));
        logger.info(String.format("Code for 0 seconds: %s", otp));
        logger.info(String.format("Code for 60 seconds: %s", otpGenerator.at(now.plusSeconds(60))));
        logger.info(String.format("Code for 120 seconds: %s", otpGenerator.at(now.plusSeconds(120))));
        logger.info(String.format("Code for 180 seconds: %s", otpGenerator.at(now.plusSeconds(180))));
        logger.info(String.format("Code for 240 seconds: %s", otpGenerator.at(now.plusSeconds(240))));
        logger.info(String.format("Code for 300 seconds: %s", otpGenerator.at(now.plusSeconds(300))));
        logger.info(String.format("Code for 360 seconds: %s", otpGenerator.at(now.plusSeconds(360))));
        logger.info(String.format("Code for 420 seconds: %s", otpGenerator.at(now.plusSeconds(420))));
        logger.info(String.format("Code for 480 seconds: %s", otpGenerator.at(now.plusSeconds(480))));
        logger.info(String.format("Code for 540 seconds: %s", otpGenerator.at(now.plusSeconds(540))));
        logger.info(String.format("Code for 600 seconds: %s", otpGenerator.at(now.plusSeconds(600))));

        return otp;
    }

    private TOTP getOtpGenerator(String emailAddress)
    {
        var secret = getSecret(emailAddress);

        return createOtpGenerator(secret);
    }

    private byte[] getSecret(String emailAddress)
    {
        var secret = otpKeyCache.getIfPresent(emailAddress);

        if (secret == null)
        {
            secret = SecretGenerator.generate();
            otpKeyCache.put(emailAddress, secret);
        }

        return secret;
    }

    private TOTP createOtpGenerator(byte[] secret)
    {
        var builder = new TOTP.Builder(secret);

        return builder.withPasswordLength(6)
                      .withAlgorithm(HMACAlgorithm.SHA512)
                      .withPeriod(Duration.ofMillis(otpDurationMs))
                      .build();
    }

The attached image shows the code in execution. Please note that this is the first time this method is executed, so it is not getting a "cached" secret.

otp-code-mismatch2

BastiaanJansen commented 2 years ago

Hi,

The 15-minute timer does not start when you generate your first code. It is completely dependent on the time you generate a code to know how long it will be valid until the next code is generated.

For each generate method call, a counter is generated from the period with the following code:

System.currentTimeMillis() / period.toMillis();

So, this counter is the same for every 30 seconds (with a period of 30 seconds). This counter with the same secret will always generate the same token. After the 30 second time window is over, the counter will be 1 bigger than the previous. Because it depends on the system time, it is not always the case that when a token generated on 10:00:00 will be invalid after 10:00:30.