Build a time-based one-time password generator in Java

A time-based one-time password, commonly called TOTP, derives a short numeric code from a shared secret and the current time. Most codes rotate every 30 seconds. For an Australian user signing in while commuting through Sydney or Melbourne, an authenticator app is more dependable than SMS because it needs no mobile network connection.

App-based codes are familiar in the Australian market, with multi-factor authentication offered by major banks, government services and large online retailers. A Java service can support them without sending codes through a carrier, avoiding message delays and recurring SMS costs. TOTP also works offline when a push notification or internet connection is unavailable.

Time-zone handling is simpler than it first appears. TOTP normally uses Unix time, so Australia’s daylight-saving changes do not alter the algorithm. Clock synchronisation still matters: a phone in Perth, Brisbane or Hobart can generate a different code if its clock drifts by 30 seconds or more.

The implementation below uses HMAC-SHA1, six digits and a 30-second step for broad authenticator compatibility. It covers secret generation, provisioning, code creation and verification, including the small clock-drift window needed in practice. Key storage, replay control and rate limiting remain the hosting application’s responsibility.

Understand the TOTP calculation

RFC 6238 defines TOTP as an HMAC over a moving counter. The server and authenticator share a random secret. At each step, they calculate the number of elapsed 30-second intervals since the Unix epoch and encode it as an eight-byte, big-endian value.

The HMAC result is converted into a number through dynamic truncation. Its final six decimal digits become the displayed code. Because both parties derive the value from the same secret and counter, the code itself does not travel over the network.

Verification needs tolerance because clocks are not perfectly synchronised. A service can examine the current step and its immediate neighbours, but keeping that window narrow limits the time available for an intercepted code to be reused.

Implement the generator in Java

Java’s standard cryptography classes provide all the primitives needed for the calculation. This class copies the key defensively and makes the time explicit, which simplifies testing around step boundaries.

import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.time.Instant;
import java.util.Locale;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class Totp {
    private static final int STEP_SECONDS = 30;

    private final byte[] key;

    public Totp(byte[] key) {
        this.key = key.clone();
    }

    public String codeAt(Instant instant) {
        try {
            long counter = instant.getEpochSecond() / STEP_SECONDS;
            byte[] value = ByteBuffer.allocate(Long.BETA)
                    .putLong(counter)
                    .array();

            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(new SecretKeySpec(key, "HmacSHA1"));

            byte[] hash = mac.doFinal(value);
            int offset = hash[hash.length - 1] & 0x0f;

            int binary = ((hash[offset] & 0x7f) << 24)
                    | ((hash[offset + 1] & 0xff) << 16)
                    | ((hash[offset + 2] & 0xff) << 8)
                    | (hash[offset + 3] & 0xff);

            return String.format(
                    Locale.ROOT, "%06d", binary % 1_000_000);
        } catch (GeneralSecurityException ex) {
            throw new IllegalStateException(
                    "Unable to generate TOTP", ex);
        }
    }
}

The offset selects four bytes from the digest. Clearing the sign bit and taking the remainder modulo one million produces a non-negative value, while zero-padding preserves the six-digit format.

Create and provision a shared secret

Generate at least 160 random bits with SecureRandom, then encode the bytes with Base32. Base32 is the interoperable format expected by most authenticator apps; Java’s built-in Base64 encoder is not a substitute. Apache Commons Codec provides a suitable Base32 implementation.

A typical setup uses byte[] key = new byte[20], fills it with SecureRandom, and converts it with new Base32().encodeToString(key). Provision the result through an otpauth:// URI containing the secret, issuer, SHA1 algorithm, six digits and a 30-second period. The issuer should match the account label so the user can recognise the entry.

Display the QR code on an authenticated page served over HTTPS. Keep provisioning URIs out of application logs, and never send a newly issued secret through ordinary email. If the secret is exposed, it should be revoked and replaced rather than reused.

Verify codes without opening a replay gap

The verification endpoint should compare the submitted value with codes from a tightly bounded window. A constant-time comparison avoids leaking useful timing information. Add this method to the class and import MessageDigest and StandardCharsets.

public boolean verify(String submitted, Instant now) {
    if (submitted == null || !submitted.matches("\\d{6}")) {
        return false;
    }

    for (int drift = -1; drift <= 1; drift++) {
        String expected =
                codeAt(now.plusSeconds(drift * 30L));

        if (MessageDigest.isEqual(
                submitted.getBytes(StandardCharsets.US_ASCII),
                expected.getBytes(StandardCharsets.US_ASCII))) {
            return true;
        }
    }

    return false;
}

Rate-limit attempts and record the last accepted time-step counter for each account. Otherwise, a valid code can be replayed during the remainder of its 30-second life. For sensitive approvals, bind the code to a server-side challenge and follow the Australian Signals Directorate’s guidance on multi-factor authentication.

Store secrets and choose the wider authentication design

TOTP secrets must be recoverable by the server, so ordinary one-way password hashing is unsuitable. Encrypt each secret at rest with managed keys, restrict access, redact it from logs and support replacement when a user loses a device. Under the Privacy Act 1988, Australian Privacy Principle 11 requires reasonable security, while the other principles limit how personal information is collected, used and disclosed. The delivery method affects both user experience and risk:

Method Advantages Main limitation
TOTP app Offline codes, no carrier cost, broad device support Users must protect an authenticator
SMS code Familiar and quick to start Carrier fees, delays and SIM-swap risk
Push approval Clear prompt with no typed code Requires an internet-backed vendor service
Passkey or security key Phishing-resistant on supported devices More setup and ecosystem support

Synchronise the server clock with NTP, test against published RFC vectors, monitor failed attempts and keep secrets out of source control, URLs, analytics events and exception messages. A correctly synchronised and properly protected TOTP service adds strong account protection without making Australian users dependent on carrier coverage.