forked from thijooree/android
57 lines
2.0 KiB
Kotlin
57 lines
2.0 KiB
Kotlin
package sh.sar.basedbank.util
|
|
|
|
import javax.crypto.Mac
|
|
import javax.crypto.spec.SecretKeySpec
|
|
import kotlin.experimental.and
|
|
|
|
object Totp {
|
|
|
|
/**
|
|
* Generate a 6-digit TOTP code from a Base32-encoded secret (RFC 6238 / RFC 4226).
|
|
* Uses HmacSHA1, 30-second window, 6 digits — matching standard authenticator apps.
|
|
*/
|
|
fun generate(base32Secret: String, digits: Int = 6, periodSeconds: Long = 30): String {
|
|
val key = base32Decode(base32Secret.uppercase().replace(" ", "").replace("-", ""))
|
|
val counter = System.currentTimeMillis() / 1000L / periodSeconds
|
|
val otp = hotp(key, counter, digits)
|
|
return otp.toString().padStart(digits, '0')
|
|
}
|
|
|
|
private fun hotp(key: ByteArray, counter: Long, digits: Int): Int {
|
|
val msg = ByteArray(8) { i -> ((counter shr ((7 - i) * 8)) and 0xFF).toByte() }
|
|
val mac = Mac.getInstance("HmacSHA1")
|
|
mac.init(SecretKeySpec(key, "HmacSHA1"))
|
|
val hash = mac.doFinal(msg)
|
|
|
|
val offset = (hash[hash.size - 1] and 0x0F).toInt()
|
|
val code = ((hash[offset].toInt() and 0x7F) shl 24) or
|
|
((hash[offset + 1].toInt() and 0xFF) shl 16) or
|
|
((hash[offset + 2].toInt() and 0xFF) shl 8) or
|
|
(hash[offset + 3].toInt() and 0xFF)
|
|
|
|
val modulus = Math.pow(10.0, digits.toDouble()).toInt()
|
|
return code % modulus
|
|
}
|
|
|
|
private val BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
|
|
|
private fun base32Decode(input: String): ByteArray {
|
|
val clean = input.trimEnd('=')
|
|
val output = ByteArray(clean.length * 5 / 8)
|
|
var buffer = 0
|
|
var bitsLeft = 0
|
|
var outIndex = 0
|
|
for (c in clean) {
|
|
val v = BASE32_CHARS.indexOf(c)
|
|
require(v >= 0) { "Invalid Base32 character: $c" }
|
|
buffer = (buffer shl 5) or v
|
|
bitsLeft += 5
|
|
if (bitsLeft >= 8) {
|
|
bitsLeft -= 8
|
|
output[outIndex++] = ((buffer shr bitsLeft) and 0xFF).toByte()
|
|
}
|
|
}
|
|
return output
|
|
}
|
|
}
|