jchambers / java-otp

A one-time password (HOTP/TOTP) library for Java
MIT License
452 stars 122 forks source link

Time step is not always honored #41

Closed davija closed 2 years ago

davija commented 2 years ago

Java version:

openjdk 17.0.3 2022-04-19
OpenJDK Runtime Environment (build 17.0.3+7-Ubuntu-0ubuntu0.22.04.1)
OpenJDK 64-Bit Server VM (build 17.0.3+7-Ubuntu-0ubuntu0.22.04.1, mixed mode, sharing)

Library version: 0.3.1 OS: Ubuntu Linux

I have had issues with the library not being consistent in how long time intervals are handled.

In my use case, I need to have the time interval valid for 15 minutes. In some cases it generates the correct code for the entire 15 minute interval, in other cases the code is only valid for 5 minutes before another code is generated.

Here is the code I am using:

var key = getKey(emailAddress);
var now = Instant.now();
var otp = otpGenerator.generateOneTimePasswordString(key, now);

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

And this is one of the cases where I have called it and it generated a different code. Notice in this case, the message that indicates a new key was created so it isn't the case that this was the same key as a previous instance (and yes, I am aware of the typo in the first print statement that is trying to print the key instead of the code).

(image removed and added in a different comment)

davija commented 2 years ago

otp-code-mismatch

jchambers commented 2 years ago

This is working as expected.

TOTP generators measure time relative to a fixed reference point (the UNIX epoch). Here's the specific implementation: https://github.com/jchambers/java-otp/blob/27fd2d730fc6e91b41683a8fd338b08f33ca4743/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java#L126-L139

When you specify a time step, you're essentially dividing time from that interval into chunks of the same size. If you specify a fifteen-minute time step, you can expect the code to rotate at noon, 12:15, 12:30, 12:45, 13:00, and so on. The reference time is unaffected by when you (for example) create your TimeBasedOneTimePasswordGenerator instance. This behavior allows remote clients to know what code to use without knowing when the TimeBasedOneTimePasswordGenerator instance was created on the server.

For additional details, please see RFC 6238 - TOTP: Time-Based One-Time Password Algorithm.