OTP Code Generator — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// otp-code-generator — Kotlin 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.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/** TOTP options — mirrors `Options` in the Go twin. */
data class TotpOptions(
val secret: String, // Base32 (RFC 4648); spaces/case tolerated
val digits: Int = 6, // <= 0 falls back to 6 (withDefaults in the Go twin)
val period: Int = 30, // seconds, <= 0 falls back to 30
)
object Otp {
/**
* TOTP at [timestampMs] (ms since epoch) — `Generate()` in the Go twin
* (RFC 6238). Null on an invalid secret, like generateTotp() in the TS lib.
*/
fun generate(opts: TotpOptions, timestampMs: Long): String? {
val digits = if (opts.digits > 0) opts.digits else 6
val period = if (opts.period > 0) opts.period else 30
val key = decodeSecret(opts.secret) ?: return null
// Unsigned division mirrors the uint64 cast in the Go twin (JDK static).
val counter = java.lang.Long.divideUnsigned(timestampMs, 1000) / period // RFC 6238 §4.2
val msg = ByteArray(8) // big-endian counter, RFC 4226 §5.2
for (i in 0..7) msg[i] = (counter ushr (8 * (7 - i))).toByte()
val mac = Mac.getInstance("HmacSHA1") // RFC 2104 — the Go twin's crypto/hmac
mac.init(SecretKeySpec(key, "HmacSHA1"))
val sum = mac.doFinal(msg)
val off = sum.last().toInt() and 0x0f // dynamic truncation, RFC 4226 §5.4
val bin = ((sum[off].toInt() and 0x7f) shl 24) or ((sum[off + 1].toInt() and 0xff) shl 16) or
((sum[off + 2].toInt() and 0xff) shl 8) or (sum[off + 3].toInt() and 0xff)
var mod = 1UL
repeat(digits) { mod *= 10u }
return (bin.toULong() % mod).toString().padStart(digits, '0')
}
/**
* `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).
*/
fun validate(token: String, opts: TotpOptions, timestampMs: Long): Boolean {
val period = if (opts.period > 0) opts.period else 30
val window = period * 1000L
return longArrayOf(timestampMs, timestampMs - window, timestampMs + window).any { ts ->
val candidate = generate(opts, ts)
candidate != null && MessageDigest.isEqual(candidate.toByteArray(), token.toByteArray())
}
}
/**
* decodeSecret() in the Go twin: strip whitespace, uppercase, strip trailing
* '=' padding, then RFC 4648 decode. Null when the secret is invalid.
*/
private fun decodeSecret(secret: String): ByteArray? {
val norm = secret.filterNot { it.isWhitespace() }.uppercase().trimEnd('=')
val out = ByteArrayOutputStream()
var acc = 0
var bits = 0
for (c in norm) {
val v = when (c) {
in 'A'..'Z' -> c - 'A'
in '2'..'7' -> c - '2' + 26
else -> return null
}
acc = (acc shl 5) or v
bits += 5
if (bits >= 8) {
bits -= 8
out.write((acc shr bits) and 0xff)
}
}
return out.toByteArray()
}
}
fun main() {
// RFC 6238 appendix-B vectors (SHA1, 8 digits, 30s):
// secret = base32("12345678901234567890").
val o8 = TotpOptions("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", digits = 8)
listOf(
59_000L to "94287082",
1_111_111_109_000L to "07081804",
1_234_567_890_000L to "89005924",
2_000_000_000_000L to "69279037",
).forEach { (ts, want) ->
println("ts=${ts.toString().padStart(13)} token=${Otp.generate(o8, ts)} want=$want")
}
// Tool defaults (6 digits) + whitespace/case-tolerant secret entry.
val o6 = TotpOptions("gezd gnbv gy3t qojq gezd gnbv gy3t qojq")
val token = Otp.generate(o6, 59_000L)
println(
"6-digit=$token validate(now)=${Otp.validate(token!!, o6, 59_000L)} " +
"validate(+1)=${Otp.validate(token, o6, 89_000L)} " +
"validate(wrong)=${Otp.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 →