
Lightweight RFC-compliant TOTP and HOTP generator supporting SHA-1/256/512, Base32 validation/decoding, native HMAC backends, and configurable digits/time-step, with zero third-party dependencies.
A lightweight Kotlin Multiplatform library for generating TOTP & HOTP one-time passwords.
RFC-compliant TOTP (RFC 6238) and HOTP (RFC 4226) for Kotlin Multiplatform. Use it from common code on every platform, whether you are building an authenticator app or adding two-factor login to a server. Zero third-party dependencies: it uses each platform's native cryptography.
Flow (beauthy-sdk-coroutines) or Compose state (beauthy-sdk-compose) that ticks every secondotpauth:// URIs: parse authenticator QR codes and build them for enrollmentcommonMain
beauthy-sdk beyond the Kotlin standard librarykotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.elliuqahs:beauthy-sdk:0.2.0")
// Optional: live codes as a Flow
implementation("io.github.elliuqahs:beauthy-sdk-coroutines:0.2.0")
// Optional: live codes as Compose state (includes beauthy-sdk-coroutines)
implementation("io.github.elliuqahs:beauthy-sdk-compose:0.2.0")
}
}
}dependencies {
implementation("io.github.elliuqahs:beauthy-sdk:0.2.0")
}All examples below are common code and run unchanged on every supported platform.
import io.github.elliuqahs.beauthy.*
// TOTP (defaults: SHA-1, 6 digits, 30 seconds)
val totp = Totp(secret = "JBSWY3DPEHPK3PXP")
val current = totp.current()
current.code // "861370"
current.formatted() // "861 370"
current.remainingSeconds // 15
current.progress // 0.5
// Custom parameters
val totp256 = Totp(
secret = "JBSWY3DPEHPK3PXP",
algorithm = HmacAlgorithm.SHA256,
digits = 8,
period = 60
)
// HOTP
val hotp = Hotp(secret = "JBSWY3DPEHPK3PXP")
val hotpCode = hotp.generate(counter = 42)totp.generate() returns just the code string. totp.at(timestampMillis) and the timestampMillis parameters let you use your own clock, for example in tests.
A code is valid for one period (30 seconds by default). current() returns the code with its countdown and progress, all computed from the same instant. The optional artifacts keep it up to date for you, ticking at the start of every second so the countdown follows the clock and the code changes as soon as a new period begins.
Compose Multiplatform (beauthy-sdk-compose):
@Composable
fun AccountRow(secret: String) {
val current by rememberTotpCode(secret)
Text(current.formatted()) // "861 370"
Text("${current.remainingSeconds}s") // "15s"
LinearProgressIndicator(progress = { current.progress })
}rememberTotpCode(secret, algorithm, digits, period) throws for an invalid secret, so validate user input with Base32.isValid first, or pass a Totp you created yourself: rememberTotpCode(totp).
Coroutines (beauthy-sdk-coroutines), for example in a ViewModel:
class AccountViewModel(secret: String) : ViewModel() {
private val totp = Totp(secret)
val code: StateFlow<TotpCode> = totp.codes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), totp.current())
}Neither: call current() once a second yourself, or schedule the next refresh for current.expiresAtMillis if you only need to know when the code changes.
Codes and countdowns come only from the clock, so the server and the app agree as long as both clocks are right. A phone whose clock is off by more than about 30 seconds produces codes the server rejects. Measure the error once against a trusted time, such as the Date header of any HTTPS response, and pass it in:
val offset = ClockOffset.from(serverTimeMillis) // positive when the device is behind
val totp = Totp(secret, clockOffsetMillis = offset)
val totpFromQr = OtpAuthUri.parse(qrText).toTotp(clockOffsetMillis = offset)
val current by rememberTotpCode(secret, clockOffsetMillis = offset) // ComposeThe offset applies wherever the current time is used (current(), generate(), verify(), codes()); timestamps you pass explicitly are used as given.
val uri = OtpAuthUri.parseOrNull(scannedText) ?: return // not an otpauth:// QR code
println("${uri.issuer} - ${uri.accountName}")
val code = when (uri.type) {
OtpType.TOTP -> uri.toTotp().generate()
OtpType.HOTP -> uri.toHotp().generate(uri.counter)
}// Enrollment: store the secret, show the URI as a QR code
val secret = Secret.generate()
val qrContent = OtpAuthUri(
type = OtpType.TOTP,
secret = secret,
accountName = "alice@example.com",
issuer = "Example"
).toUriString()
// Login: accept the current code or one period either side
val valid = Totp(secret).verify(userInput)A TOTP code stays valid for its whole window, so remember which time step each user last logged in with and reject reuse. Hotp.verify returns the matched counter; store it plus one as the next expected counter.
if (Base32.isValid(userInput)) {
val totp = Totp(userInput)
}Constructors throw IllegalArgumentException for an invalid secret, digits outside 6..9, or a non-positive period.
The full API reference is on javadoc.io for beauthy-sdk, beauthy-sdk-coroutines and beauthy-sdk-compose.
More examples are in samples. Run them all with ./gradlew :samples:run.
| Platform | Targets | HMAC backend |
|---|---|---|
| Android | minSdk 24 | javax.crypto.Mac |
| JVM | Java 11+ | javax.crypto.Mac |
| iOS | arm64, simulatorArm64, x64 | CommonCrypto CCHmac
|
0.2.0 moves the API to the io.github.elliuqahs.beauthy package and removes the need for a platform HmacProvider. The old com.maoungedev.beauthy.core.crypto API still works but is deprecated and will be removed in a future release.
| 0.1.x | 0.2.0 |
|---|---|
TotpGenerator(JvmHmacProvider()) / TotpGenerator(IosHmacProvider())
|
not needed |
generator.generate(secret, now, digits, period, algorithm) |
Totp(secret, algorithm, digits, period).generate(now) |
generator.generateHotp(secret, counter, digits, algorithm) |
Hotp(secret, algorithm, digits).generate(counter) |
generator.remainingSeconds(now, period) |
Totp(secret, period = period).remainingSeconds(now) |
com.maoungedev.beauthy.core.crypto.Base32 |
io.github.elliuqahs.beauthy.Base32 |
com.maoungedev.beauthy.core.crypto.HmacAlgorithm |
io.github.elliuqahs.beauthy.HmacAlgorithm |
See CHANGELOG.md for what changed in each version.
Support it by joining stargazers for this repository. ⭐
Copyright 2025 elliuqahs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
A lightweight Kotlin Multiplatform library for generating TOTP & HOTP one-time passwords.
RFC-compliant TOTP (RFC 6238) and HOTP (RFC 4226) for Kotlin Multiplatform. Use it from common code on every platform, whether you are building an authenticator app or adding two-factor login to a server. Zero third-party dependencies: it uses each platform's native cryptography.
Flow (beauthy-sdk-coroutines) or Compose state (beauthy-sdk-compose) that ticks every secondotpauth:// URIs: parse authenticator QR codes and build them for enrollmentcommonMain
beauthy-sdk beyond the Kotlin standard librarykotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.elliuqahs:beauthy-sdk:0.2.0")
// Optional: live codes as a Flow
implementation("io.github.elliuqahs:beauthy-sdk-coroutines:0.2.0")
// Optional: live codes as Compose state (includes beauthy-sdk-coroutines)
implementation("io.github.elliuqahs:beauthy-sdk-compose:0.2.0")
}
}
}dependencies {
implementation("io.github.elliuqahs:beauthy-sdk:0.2.0")
}All examples below are common code and run unchanged on every supported platform.
import io.github.elliuqahs.beauthy.*
// TOTP (defaults: SHA-1, 6 digits, 30 seconds)
val totp = Totp(secret = "JBSWY3DPEHPK3PXP")
val current = totp.current()
current.code // "861370"
current.formatted() // "861 370"
current.remainingSeconds // 15
current.progress // 0.5
// Custom parameters
val totp256 = Totp(
secret = "JBSWY3DPEHPK3PXP",
algorithm = HmacAlgorithm.SHA256,
digits = 8,
period = 60
)
// HOTP
val hotp = Hotp(secret = "JBSWY3DPEHPK3PXP")
val hotpCode = hotp.generate(counter = 42)totp.generate() returns just the code string. totp.at(timestampMillis) and the timestampMillis parameters let you use your own clock, for example in tests.
A code is valid for one period (30 seconds by default). current() returns the code with its countdown and progress, all computed from the same instant. The optional artifacts keep it up to date for you, ticking at the start of every second so the countdown follows the clock and the code changes as soon as a new period begins.
Compose Multiplatform (beauthy-sdk-compose):
@Composable
fun AccountRow(secret: String) {
val current by rememberTotpCode(secret)
Text(current.formatted()) // "861 370"
Text("${current.remainingSeconds}s") // "15s"
LinearProgressIndicator(progress = { current.progress })
}rememberTotpCode(secret, algorithm, digits, period) throws for an invalid secret, so validate user input with Base32.isValid first, or pass a Totp you created yourself: rememberTotpCode(totp).
Coroutines (beauthy-sdk-coroutines), for example in a ViewModel:
class AccountViewModel(secret: String) : ViewModel() {
private val totp = Totp(secret)
val code: StateFlow<TotpCode> = totp.codes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), totp.current())
}Neither: call current() once a second yourself, or schedule the next refresh for current.expiresAtMillis if you only need to know when the code changes.
Codes and countdowns come only from the clock, so the server and the app agree as long as both clocks are right. A phone whose clock is off by more than about 30 seconds produces codes the server rejects. Measure the error once against a trusted time, such as the Date header of any HTTPS response, and pass it in:
val offset = ClockOffset.from(serverTimeMillis) // positive when the device is behind
val totp = Totp(secret, clockOffsetMillis = offset)
val totpFromQr = OtpAuthUri.parse(qrText).toTotp(clockOffsetMillis = offset)
val current by rememberTotpCode(secret, clockOffsetMillis = offset) // ComposeThe offset applies wherever the current time is used (current(), generate(), verify(), codes()); timestamps you pass explicitly are used as given.
val uri = OtpAuthUri.parseOrNull(scannedText) ?: return // not an otpauth:// QR code
println("${uri.issuer} - ${uri.accountName}")
val code = when (uri.type) {
OtpType.TOTP -> uri.toTotp().generate()
OtpType.HOTP -> uri.toHotp().generate(uri.counter)
}// Enrollment: store the secret, show the URI as a QR code
val secret = Secret.generate()
val qrContent = OtpAuthUri(
type = OtpType.TOTP,
secret = secret,
accountName = "alice@example.com",
issuer = "Example"
).toUriString()
// Login: accept the current code or one period either side
val valid = Totp(secret).verify(userInput)A TOTP code stays valid for its whole window, so remember which time step each user last logged in with and reject reuse. Hotp.verify returns the matched counter; store it plus one as the next expected counter.
if (Base32.isValid(userInput)) {
val totp = Totp(userInput)
}Constructors throw IllegalArgumentException for an invalid secret, digits outside 6..9, or a non-positive period.
The full API reference is on javadoc.io for beauthy-sdk, beauthy-sdk-coroutines and beauthy-sdk-compose.
More examples are in samples. Run them all with ./gradlew :samples:run.
| Platform | Targets | HMAC backend |
|---|---|---|
| Android | minSdk 24 | javax.crypto.Mac |
| JVM | Java 11+ | javax.crypto.Mac |
| iOS | arm64, simulatorArm64, x64 | CommonCrypto CCHmac
|
0.2.0 moves the API to the io.github.elliuqahs.beauthy package and removes the need for a platform HmacProvider. The old com.maoungedev.beauthy.core.crypto API still works but is deprecated and will be removed in a future release.
| 0.1.x | 0.2.0 |
|---|---|
TotpGenerator(JvmHmacProvider()) / TotpGenerator(IosHmacProvider())
|
not needed |
generator.generate(secret, now, digits, period, algorithm) |
Totp(secret, algorithm, digits, period).generate(now) |
generator.generateHotp(secret, counter, digits, algorithm) |
Hotp(secret, algorithm, digits).generate(counter) |
generator.remainingSeconds(now, period) |
Totp(secret, period = period).remainingSeconds(now) |
com.maoungedev.beauthy.core.crypto.Base32 |
io.github.elliuqahs.beauthy.Base32 |
com.maoungedev.beauthy.core.crypto.HmacAlgorithm |
io.github.elliuqahs.beauthy.HmacAlgorithm |
See CHANGELOG.md for what changed in each version.
Support it by joining stargazers for this repository. ⭐
Copyright 2025 elliuqahs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.