Random 6 digit number generator

Uniform six-digit codes over the full 000000 to 999999 range, the length RFC 4226 sets for one-time passwords.

G

Six digits because the standard says six

One-time codes are six digits because RFC 4226, which defines HOTP, requires implementations to extract at least a six-digit code. RFC 6238 builds TOTP on top of that by replacing the counter with the current time divided into steps, and recommends a default step of 30 seconds. Every authenticator app you have used follows both documents, which is why the code is six digits and why it changes twice a minute.

Six digits gives 1,000,000 values and 6 x log2(10) = 19.93 bits. On our strength scale that is the bottom bucket, and it is correct that it is: a million guesses is trivial work for any machine. The label is not an indictment of TOTP. It is a statement that the code is not the whole control.

19.93 bits is the entire budget, so the limits around it matter

A single guess against a six-digit code succeeds with probability one in a million. That sounds comfortable until you count how many guesses an attacker gets. With no throttling, a script submitting a few hundred codes per second exhausts the space in under an hour, and it does not even need to finish, because the expected time to a hit is half of that.

Two things stop it. The verification window is short, typically the current 30-second step plus at most one step either side to allow for clock drift, so guesses made against an expired code are wasted. And the server must count failures per account and lock or delay after a handful. RFC 6238 addresses this directly in its security considerations, requiring throttling of failed verification attempts. If your implementation has a 30-second window and no attempt counter, the code is not protecting the account.

Chance of at least one duplicate in a batch of independent 6-digit codes
Codes drawnProbability of a repeat
1000.5%
1421.0%
50011.7%
1,17850.0%

A six-digit code is not a TOTP secret

These two get confused constantly, and the difference is worth stating plainly. The TOTP secret is a shared key, held by both the server and the authenticator app, conventionally random bytes encoded in base32 and delivered in a QR code. The six-digit code is derived from that secret and the current time by an HMAC, truncated down to six digits.

So this page generates codes, not secrets. If you need a secret to enrol a test account, you need a random byte string of the length your library expects, encoded appropriately, not a six-digit value. Using a six-digit number as a shared secret would give the entire TOTP scheme 19.93 bits of key material, which defeats it completely.

The verifying server derives the expected code from the secret and the clock. It never stores or transmits the code itself, which is why a code is safe to display and a secret is not.

Leading zeros break more OTP implementations than anything else

A six-digit code is a string. 047318 is a valid code and one code in ten begins with a zero. The classic bug is a service that stores or transmits the code as an integer: 047318 becomes 47318, and now either the comparison fails for a tenth of your users, or the code is compared numerically and both 047318 and 47318 are accepted, which quietly widens the accepted set.

The upstream version of the same mistake is a generator that picks an integer between 100000 and 999999. That covers 900,000 of the 1,000,000 codes, drops the entropy from 19.93 to 19.78 bits, and guarantees that the first digit is never zero, which an attacker enumerating codes can use to skip a tenth of the space. This tool draws each digit independently, so every one of the million codes appears at the same rate.

Duplicates are a separate matter. Independent draws from a million values collide at 1% after 142 codes and at 50% after 1,178, which is exactly why a batch generator that promises unique output has to implement uniqueness rather than trust the space. One page-1 competitor promises unique output in its own meta description and produced 14 collisions in 5,000 six-digit codes, against a birthday expectation of about 12.5. Their randomness is fine. The promise is what is missing.

Doing this in your own code

JavaScript
// 6 independent digits, uniform, leading zeros preserved.
// 250 is Math.floor(256 / 10) * 10. Bytes at or above it are thrown away,
// which is what keeps every digit at exactly 1 in 10.
function randomDigits(n) {
  const buf = new Uint8Array(1);
  let out = "";
  while (out.length < n) {
    crypto.getRandomValues(buf);
    if (buf[0] >= 250) continue;
    out += buf[0] % 10;
  }
  return out;
}

randomDigits(6);   // a 6-character string, never a number

The usual shortcut, Math.floor(Math.random() * 9e5) + 1e5, cannot produce a leading zero and draws from a PRNG whose state is recoverable from its output.

Python 3.6+
import secrets

# Draw the digits. This never becomes an int, so leading zeros survive.
"".join(secrets.choice("0123456789") for _ in range(6))

# Or draw one integer and pad it back to width.
f"{secrets.randbelow(10 ** 6):06d}"

random.randint() uses the Mersenne Twister and is not a security boundary. secrets draws from os.urandom.

Go
import (
    "crypto/rand"
    "fmt"
    "math/big"
)

limit := new(big.Int).Exp(big.NewInt(10), big.NewInt(6), nil)
v, err := rand.Int(rand.Reader, limit)
if err != nil {
    panic(err)
}

// The width verb is the part that keeps the leading zeros.
fmt.Printf("%06d\n", v)
Java
import java.security.SecureRandom;

SecureRandom rnd = new SecureRandom();
StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < 6; i++) {
    sb.append(rnd.nextInt(10));
}
String value = sb.toString();

nextInt(bound) already rejects the unusable top of the range, so it is unbiased for a bound of 10.

PHP 7+
// random_int() is backed by the system CSPRNG.
// rand() and mt_rand() are not, and must not be used here.
$value = str_pad((string) random_int(0, 10 ** 6 - 1), 6, '0', STR_PAD_LEFT);
Shell
# 6 digits straight from the kernel CSPRNG.
# tr keeps only ASCII digits, so every byte that survives is uniform over 0-9.
LC_ALL=C tr -dc '0-9' < /dev/urandom | head -c 6; echo

Questions

Is a 6-digit OTP secure?

It carries 19.93 bits, which by itself is weak. It is secure in practice because the code expires within a short window, usually 30 seconds plus a step of drift tolerance, and because the server limits failed attempts. RFC 6238 requires that throttling. Remove either the window or the counter and six digits stops being a control.

Is this a TOTP secret key generator?

No. This produces the six-digit code, not the shared secret. A TOTP secret is a random byte string encoded in base32 and scanned from a QR code; the code is derived from it and the current time. Using a six-digit value as the secret would reduce the whole scheme to 19.93 bits.

Why do authenticator codes change every 30 seconds?

RFC 6238 defines TOTP as HOTP with the counter replaced by the current Unix time divided by a time step, and recommends 30 seconds as the default step. A shorter step means fewer usable guesses per code; a longer one is friendlier to clock drift. Thirty seconds is the compromise almost everyone shipped.

Can a 6-digit code start with 0?

Yes, and one in ten does. Codes are strings, not numbers. Any generator that draws an integer from 100000 to 999999 excludes 100,000 valid codes and makes the leading digit non-uniform.

Can I use these codes for real two-factor authentication?

Only for testing. Real two-factor codes must be derived from a secret shared with the verifying server, or the server has nothing to check them against. A standalone random code is useful as a fixture, a placeholder in a UI, or a manually delivered confirmation code that the server generates and stores itself.

How many codes can I generate before one repeats?

The chance of a duplicate is 1% at 142 codes and 50% at 1,178. If you are issuing codes that must be distinct, check for collisions rather than assuming a million values is enough.

Related generators