OTP Code Generator — Java source
Generate time-based one-time passwords (RFC 6238 TOTP) from a Base32 secret, with selectable algorithm, digit count, and period - updating live, entirely in your browser.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// otp-code-generator — Java port: TOTP (RFC 6238) / HOTP (RFC 4226) generator.
//
// Display port of the CosmoDev OTP Code Generator tool — same contract as
// cli/otp-code-generator/otp-code-generator.go (the live Go twin) and
// src/lib/otp.ts (canonical TypeScript, which wraps `otpauth`). HmacSHA1 comes
// from the JDK (javax.crypto); base32 is hand-rolled. Only the SHA1 path is
// ported — the TS default and the algorithm every published RFC 6238/4226 test
// vector uses; SHA256/512 follow the same HMAC construction in the Go twin.
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
final class OtpCodeGenerator {
// Package-private: the polyglot display filename is java.java, so the
// top-level class cannot be public (JLS §7.6 filename rule).
private OtpCodeGenerator() {}
/** TOTP options — mirrors {@code Options} in the Go twin. */
public static final class Options {
/** Base32 (RFC 4648); spaces/case tolerated. */
public final String secret;
/** Digits; <= 0 falls back to the TS default 6. */
public final int digits;
/** Period in seconds; <= 0 falls back to the TS default 30. */
public final int period;
public Options(String secret, int digits, int period) {
this.secret = secret;
this.digits = digits;
this.period = period;
}
}
/**
* TOTP at {@code timestampMs} (ms since epoch) — {@code Generate()} in the Go
* twin (RFC 6238). Null on an invalid secret, like generateTotp() in the TS lib.
*/
public static String generate(Options opts, long timestampMs) {
int digits = opts.digits > 0 ? opts.digits : 6; // withDefaults in the Go twin
int period = opts.period > 0 ? opts.period : 30;
byte[] key = decodeSecret(opts.secret);
if (key == null) return null;
long counter = Long.divideUnsigned(timestampMs, 1000) / period; // RFC 6238 §4.2
byte[] msg = new byte[8];
for (int i = 0; i < 8; i++) msg[i] = (byte) (counter >>> (8 * (7 - i))); // RFC 4226 §5.2
byte[] sum;
try {
Mac mac = Mac.getInstance("HmacSHA1"); // RFC 2104 — the Go twin's crypto/hmac
mac.init(new SecretKeySpec(key, "HmacSHA1"));
sum = mac.doFinal(msg);
} catch (Exception e) {
return null; // HmacSHA1 ships with every JDK; unreachable in practice
}
int off = sum[sum.length - 1] & 0x0f; // dynamic truncation, RFC 4226 §5.4
int bin = ((sum[off] & 0x7f) << 24) | ((sum[off + 1] & 0xff) << 16)
| ((sum[off + 2] & 0xff) << 8) | (sum[off + 3] & 0xff);
long mod = 1;
for (int i = 0; i < digits; i++) mod *= 10;
return String.format("%0" + digits + "d", Integer.toUnsignedLong(bin) % mod);
}
/**
* {@code Validate()} in the Go twin: the current period and ±1 adjacent
* periods (otpauth's default window=1). MessageDigest.isEqual is the
* constant-time compare (the Go twin's hmac.Equal).
*/
public static boolean validate(String token, Options opts, long timestampMs) {
int period = opts.period > 0 ? opts.period : 30;
long window = (long) period * 1000;
for (long ts : new long[] {timestampMs, timestampMs - window, timestampMs + window}) {
String candidate = generate(opts, ts);
if (candidate != null
&& MessageDigest.isEqual(candidate.getBytes(StandardCharsets.US_ASCII),
token.getBytes(StandardCharsets.US_ASCII)))
return true;
}
return false;
}
/**
* decodeSecret() in the Go twin: strip whitespace, uppercase, strip trailing
* '=' padding, then RFC 4648 decode. Null when the secret is invalid.
*/
private static byte[] decodeSecret(String secret) {
StringBuilder sb = new StringBuilder();
for (char c : secret.toCharArray())
if (!Character.isWhitespace(c)) sb.append(Character.toUpperCase(c));
String s = sb.toString();
int end = s.length();
while (end > 0 && s.charAt(end - 1) == '=') end--;
s = s.substring(0, end);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int acc = 0, bits = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
int v = (c >= 'A' && c <= 'Z') ? c - 'A'
: (c >= '2' && c <= '7') ? c - '2' + 26 : -1;
if (v < 0) return null;
acc = (acc << 5) | v;
bits += 5;
if (bits >= 8) {
bits -= 8;
out.write((acc >> bits) & 0xff);
}
}
return out.toByteArray();
}
public static void main(String[] args) {
// RFC 6238 appendix-B vectors (SHA1, 8 digits, 30s):
// secret = base32("12345678901234567890").
Options o8 = new Options("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8, 30);
long[] ts = {59_000L, 1_111_111_109_000L, 1_234_567_890_000L, 2_000_000_000_000L};
String[] want = {"94287082", "07081804", "89005924", "69279037"};
for (int i = 0; i < ts.length; i++)
System.out.printf("ts=%-13d token=%s want=%s%n", ts[i], generate(o8, ts[i]), want[i]);
// Tool defaults (6 digits) + whitespace/case-tolerant secret entry.
Options o6 = new Options("gezd gnbv gy3t qojq gezd gnbv gy3t qojq", 0, 0);
String token = generate(o6, 59_000L);
System.out.printf("6-digit=%s validate(now)=%b validate(+1)=%b validate(wrong)=%b%n",
token, validate(token, o6, 59_000L), validate(token, o6, 89_000L),
validate("123789", o6, 59_000L));
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →