Skip to content

fix(security): generate OTP with SecureRandom, uniform draw, zero-padded (A2) - #136

Open
pallakartheekreddy wants to merge 1 commit into
developfrom
security/secure-random-otp
Open

fix(security): generate OTP with SecureRandom, uniform draw, zero-padded (A2)#136
pallakartheekreddy wants to merge 1 commit into
developfrom
security/secure-random-otp

Conversation

@pallakartheekreddy

Copy link
Copy Markdown
Contributor

Summary

Fixes the predictable-OTP finding A2 (HIGH) in implementation-designs/sunbird-auth.md.

The vulnerability

KeycloakSmsAuthenticatorUtil.getSmsCode generated the OTP with:

double maxValue = Math.pow(10.0, nrOfDigits);
Random r = new Random();
long code = (long) (r.nextFloat() * maxValue);
return Long.toString(code);

Three compounding defects:

  1. java.util.Random is a time-seeded LCG — not cryptographic, predictable from a couple of outputs.
  2. nextFloat()'s ~24-bit mantissa can't uniformly address a 10^8 space → gaps/clustering, some codes unreachable.
  3. Long.toString(code) drops leading zeros, shrinking effective length and leaking that the high digits were zero.

The fix

  • A shared private static final SecureRandom.
  • Uniform SECURE_RANDOM.nextInt(10^n) draw.
  • String.format("%0Nd", code) zero-padding so every OTP is exactly nrOfDigits long.
  • Guard nrOfDigits to 1..9 (fits in int); throw IllegalArgumentException (still a RuntimeException, so existing catch sites are unaffected).

Pure utility change — no Keycloak SPI contract impact. SecureRandom.nextInt is non-blocking at OTP volumes.

Verification

  • sms-provider module test-compiles under JDK 11 (exit 0; class recompiled).

Scope / deferred

A1 (OTP expiry never enforced / no first-use invalidation), A3 (attempt lockout/rate-limit), and A4 (msg91 over cleartext HTTP, authkey in URL) are separate, higher-touch changes and get their own PRs.

…ded (A2)

KeycloakSmsAuthenticatorUtil.getSmsCode used java.util.Random + nextFloat():
  Random r = new Random();
  long code = (long) (r.nextFloat() * maxValue);
  return Long.toString(code);
Three defects made the OTP guessable/undersized: (1) java.util.Random is a
time-seeded LCG, predictable from a few outputs; (2) nextFloat()'s ~24-bit mantissa
cannot uniformly address a 10^8 space (gaps/clustering); (3) Long.toString drops
leading zeros, shrinking effective length and leaking high-zero digits.

Replace with a shared SecureRandom, a uniform nextInt(10^n) draw, and %0Nd
zero-padding so every code is exactly nrOfDigits long. Guard nrOfDigits to 1..9
(10^9 fits in int) and throw IllegalArgumentException (still a RuntimeException, so
existing catch sites are unaffected).

Pure utility change, no Keycloak SPI contract impact. Verified: sms-provider module
test-compiles under JDK 11.

Implements A2 from implementation-designs/sunbird-auth.md. A1 (OTP expiry/first-use
invalidation), A3 (attempt lockout) and A4 (msg91 cleartext HTTP) are separate,
higher-touch changes deferred to their own PRs.
@pallakartheekreddy
pallakartheekreddy changed the base branch from master to develop July 27, 2026 09:49
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