Press Learn from the beginning to walk through it step by step, or Play to just watch a new code appear every window. Hover any box for a one-line reminder.
Time window T
0
Seconds left
30s
Current code
------
Verified
0 ✓
Shared secret · Kfrom QR codefixed, ~20 bytes
Time counter · Tfloor(now / 30)new value every 30s
HMAC-SHA1 ( K , T )20-byte digest
Dynamic truncationpick 4 bytesoffset from last byte
mod 1 000 000------6 digits
Serverrecompute & comparestores only K
1 / 8
secret KJBSWY3DPEHPK3PXP… (fixed, scanned from the QR code)
mod 1 000 0001357872921 mod 1000000 = 872921 ← the code you type
speed
1×
Space plays or pauses · → next lesson
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
// Runs on the phone. Same code runs on the server.
static String generateTotp(byte[] secret, long unixSeconds) throws Exception {
long counter = unixSeconds / 30; // 30-second window: this is T
byte[] msg = ByteBuffer.allocate(8).putLong(counter).array();
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(secret, "HmacSHA1"));
byte[] digest = mac.doFinal(msg); // 20 raw bytes, never a number yet
int offset = digest[digest.length - 1] & 0x0f; // last byte's low 4 bits
int binary = ((digest[offset] & 0x7f) << 24) // glue 4 bytes into one int
| ((digest[offset + 1] & 0xff) << 16)
| ((digest[offset + 2] & 0xff) << 8)
| (digest[offset + 3] & 0xff);
int otp = binary % 1_000_000; // keep the last 6 decimal digits
return String.format("%06d", otp); // zero-pad, e.g. "042315"
}
// The server stored ONLY `secret`. It never wrote the OTP anywhere.
static boolean verify(byte[] secret, String typed, long now) throws Exception {
// tolerate small clock skew: check the window before and after too
for (int drift = -1; drift <= 1; drift++) {
String expected = generateTotp(secret, now + drift * 30L);
if (constantTimeEquals(expected, typed)) return true;
}
return false; // expired or wrong
}
// compare without leaking how many digits matched via timing
static boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int diff = 0;
for (int i = 0; i < a.length(); i++) diff |= a.charAt(i) ^ b.charAt(i);
return diff == 0;
}