Skip to content

Add methods for validating one-time passwords#56

Open
jchambers wants to merge 2 commits into
mainfrom
validate_otp
Open

Add methods for validating one-time passwords#56
jchambers wants to merge 2 commits into
mainfrom
validate_otp

Conversation

@jchambers
Copy link
Copy Markdown
Owner

I've been reluctant to add methods to validate one-time passwords because the security considerations that come with validating passwords are non-trivial (how does rate-limiting work, for example?), and I didn't want to give the impression that java-otp was providing a complete solution. I think I've convinced myself to add them, though, because:

  1. There are enough nuances—particularly around passwords presented as strings—that we can get right here that it could be a legitimate service to users
  2. We can add links to the parts of the relevant RFCs that help callers understand the security considerations that go beyond just making sure passwords match
  3. Really, if somebody was going to build an OTP system that allows attackers to brute-force one-time passwords, the lack of convenient validation methods in java-otp isn't going to be the thing that saves them

*/
public boolean validateOneTimePassword(final Key key, final long counter, final String oneTimePassword) throws InvalidKeyException {
if (oneTimePassword == null) {
throw new NullPointerException("One-time password must not be null");
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're explicitly checking for null here, we should probably do it everywhere.

Copy link
Copy Markdown
Owner Author

@jchambers jchambers May 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, broadly, there are two strategies we could use for checking that the provided one-time password matches the expected password:

We can do what we're doing here where we parse the given one-time password as an integer, then compare it to the integer value of the expected password
We can generate the expected one-time password as a string, then do a direct string comparison

The integer comparison approach is locale-agnostic (Integer.parseInt can parse numbers from any character set), but a little more complicated. Its execution time is also "constant-time" in the sense that none of its execution time is dependent upon any property of the expected one-time password.

EDIT: Actually, on a second read, this isn't quite true—if we bail out early with the length check, an attacker can learn the expected length of the password by observing execution time. I'll come back and iterate on this, though reasonable minds might disagree about whether the expected password length is actually a secret. (EDIT EDIT: it's constant-time now!)

The string comparison approach is much simpler and, assuming we use something like MessageDigest#isEqual, also constant-time:

return MessageDigest.isEqual(oneTimePassword.getBytes(StandardCharsets.UTF_8),
    generateOneTimePasswordString(key, counter, locale).getBytes(StandardCharsets.UTF_8));

The problem (or maybe advantage, depending on who you ask?) is that string comparison is locale-sensitive. In other words, even though "003784" and "००३७८४" represent the same number in their respective locales, they're clearly not the same string, and a caller would have to specify which locale they're expecting for validation to pass.

I think I like the locale-agnostic approach, but am very open to feedback and suggestions!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're explicitly checking for null here, we should probably do it everywhere.

Actually, existing coverage here is solid. The main gap I was noticing was passing null for keys, but those turn into InvalidKeyExceptions, which makes sense to me.

@jchambers jchambers force-pushed the validate_otp branch 3 times, most recently from 2d69000 to 1435cb3 Compare May 25, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant