marcelkliemannel/kotlin-onetimepassword

check TOTP

Closed this issue · 2 comments

Hi Marcel, I try to use your library, in Android app but when try to use TOTP generation and check the otp in swift or java your TOTP is wrong, I also checked the otp online (ex. https://totp.danhersam.com/ ) and when check the OTP generated in swift , kotlin (with your lib) and online result that the code wrong is generated from your lib please you can give me support ?

val byteArray2 = "ABCDEFGHIJKLMNOP"!!.toByteArray(Charsets.UTF_8)
val config = TimeBasedOneTimePasswordConfig(codeDigits = 6,
hmacAlgorithm = HmacAlgorithm.SHA1,
timeStep = 30,
timeStepUnit = TimeUnit.SECONDS)
val timeBasedOneTimePasswordGenerator = TimeBasedOneTimePasswordGenerator(byteArray, config)

val OTP = timeBasedOneTimePasswordGenerator.generate()

Hi,

first of all, this library has been used for years on the server side in several applications that are used by hundreds of active users every day, who in turn are generating the codes from third-party apps. Therefore, I am confident that the library works correctly. Most of the problems are rather usage errors. In your example I see two problem areas:

Base32 Encoding

Some TOTP generators, like the page you linked, use the "Google way" to generate codes (see last part of the README.md). This means that the generator works internally with the plain text secret, but the secret is passed around as Base32 encoded. In your example ABCDEFGHIJKLMNOP is the plain text secret, which can't be used directly in some generators without encoding it to Base32 first.

If you use MySecret12 as your secret, the Base32 encoded secret would be JV4VGZLDOJSXIMJS. If you enter the latter value in your linked page, you will get the correct code as the following code would generate:

val plainTextSecret = "MySecret12".toByteArray(Charsets.UTF_8)

// This is the encoded one to use in most of the generators (Base32 is from the Apache commons codec library)
val base32EncodedSecret = Base32().encode(plainTextSecret)
println("Base32 encoded secret: ${base32EncodedSecret}")

val config = TimeBasedOneTimePasswordConfig(codeDigits = 6, hmacAlgorithm = HmacAlgorithm.SHA1, timeStep = 30, timeStepUnit = TimeUnit.SECONDS)

// Note that the plain text secret is used here.
val timeBasedOneTimePasswordGenerator = TimeBasedOneTimePasswordGenerator(plainTextSecret, config)
val otp = timeBasedOneTimePasswordGenerator1.generate()

Secret length limitation

Some generators limit the length of the plain text secret or set a fixed number of characters. The "Google Way" has a fixed value of 10 characters. Anything outside this range will not be handled correctly by some generators.

And the generator that you linked looks like it has a limit like that too, even if it is not explicitly stated: If we use the plain text secret ABCDEFGHIJKLMNOP from your example, the Base32 secret of it would be IFBEGRCFIZDUQSKKJNGE2TSPKA====== and for the secret ABCDEFGHIJKLMNOPQRST it would be IFBEGRCFIZDUQSKKJNGE2TSPKBIVEU2U. Both Base32 secrets are different, but the page generates the same codes. After some trial and error, I assume that this generator cannot correctly process a plain text secret larger than 10 characters. If you stick to a fixed plain text secret of 10 characters, the site and this library generates the same codes.

As a counter example you can use this site: https://harrisondeo.me.uk/totp/, which generates correct codes even when using a larger secret.

Overview

I've faced same problem and after I've read the @marcelkliemannel comment above, I understand what I've done wrong!
I'll leave here a code examples to new users just to make it easier for them to follow!

Code samples

Secret generation

So I've used Apache commons-codec:commons-codec lib and its class Base32 to encode&decode generated secret into base32 format! Here's a link to a maven central to get it...

private val base32 = Base32()

Than you need just to generate secret

val secret = RandomSecretGenerator().createRandomSecret(hmacAlgorithm  ALGORITHM)

Secret encoding

And now the most important part begins! To send the secret and the uri via http you need to encode the secret like in the code below! I've used a sample class OneTimePasswordSecretDto to follow possible scenario...

return OneTimePasswordSecretDto(
            secret = (base32.encodeToString(secret)),
            uri = OtpAuthUriBuilder.forTotp(base32.encode(secret))
                .label(accountName, issuer)
                .period(PERIOD_IN_SECONDS, TimeUnit.SECONDS)
                .digits(DIGITS)
                .algorithm(ALGORITHM)
                .issuer(issuer)
                .buildToString(),
        )

NOTE: To make it compatible with most of Authenticator applications it's preferred to keep config variables within the standart: algorithm=SHA1; digits=6; period=30;

Secret decoding

Now you're ready to use encoded secret to generate TOTP on server side! But keep in mind that firstly you need to decode it to ByteArray. Here's an exaple of how you can do this (I assume you have a config field of TimeBasedOneTimePasswordConfig)

public fun generateOneTimePassword(secret: String, instant: Instant): String =
    TimeBasedOneTimePasswordGenerator(
        secret = base32.decode(secret),
        config = config,
     ).generate(instant)

public fun verifyAfterLogin(secret: String, totp: String, instant: Instant): Boolean {
    return TimeBasedOneTimePasswordGenerator(
        config = config,
        secret = base32.decode(secret),
    ).isValid(totp, instant)
}