A Kotlin one-time password library to generate "Google Authenticator", "Time-based One-time Password" (TOTP) and "HMAC-based One-time Password" (HOTP) codes based on RFC 4226 and 6238.
This is a Kotlin library to generate one-time password codes for:
The implementations are based on the RFCs:
ℹ️ In this repository, changes don't happen that often and the library gets updated very rarely. However, this is not an abandoned project. Since the code is relatively simple, follows the specifications of the two RFCs, and has good test coverage, there is hardly any need to change anything.
ℹ️ If you want to use this library in conjunction with the Google Authenticator app (or similar apps), please carefully read the chapter Google Authenticator, especially the remarks regarding the Base32-encoded secret and the plain text secret length limitation. Most problems arise from not following the two remarks correctly.
This library gets used by hundreds of active users every day to generate Google Authenticator codes for several years now, so I am very confident that the code correctly generates codes.
This library is available at Maven Central:
// Groovy
implementation 'dev.turingcomplete:kotlin-onetimepassword:2.4.0'
// Kotlin
implementation("dev.turingcomplete:kotlin-onetimepassword:2.4.0")
<dependency>
<groupId>dev.turingcomplete</groupId>
<artifactId>kotlin-onetimepassword</artifactId>
<version>2.4.0</version>
</dependency>
(1) Shared secret
/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
User Server
<------------- Challenge ------------ (2)
(3) ----- One-time password (Code) ----->
The client of the user and the server must use the same code generator with the same configuration (e.g., number of code digits, hash algorithm).
If the one-time-password is used for two-factor authentication, a possible HTTP flow could look like this (even if it does not follow an official standardization):
Authorization: Basic Base64($username:$password)
to the server.401 Unauthorized
and the header WWW-Authenticate: authType="2fa"
. The value for authType
can also be made more specific to tell the client which generator should be used (e.g. HOTP, TOTP or Google). If the challenge generation is unknown in advance, this value must be transferred by appending to the header value , challenge="$challenge"
(yes, with the comma).Authorization: 2FA $code
(or a more specific generator name instead of "2FA").All three one-time password generators create a code value with a fixed length given by the code digits property in the configuration instance. The computed code gets filled with zeros at the beginning to meet this requirement. This filling is the reason why the code gets represented as a String. The RFC 4226 requires a code digits value between 6 and 8 to assure a good security trade-off. However, this library does not set any requirements for this property. But notice that through the design of the algorithm, the maximum code value is 2,147,483,647. This maximum value means that a higher code digits value than ten adds more trailing zeros to the code (in the case of 10 digits, the first number is always 0, 1, or 2).
The HOTP generator is available through the class HmacOneTimePasswordGenerator
. The constructor takes the shared secret and a configuration instance of the class HmacOneTimePasswordConfig
as arguments:
val secret = "Leia"
val config = HmacOneTimePasswordConfig(codeDigits = 8,
hmacAlgorithm = HmacAlgorithm.SHA1)
val hmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator(secret.toByteArray(), config)
The configuration instance takes the number of code digits to be generated (see the previous chapter) and the HMAC algorithm to be used (SHA1, SHA256, and SHA512 are available).
The method generate(counter: Int)
can now be used on an instance of the generator to create a HOTP code:
var code0: String = hmacOneTimePasswordGenerator.generate(counter = 0)
var code1: String = hmacOneTimePasswordGenerator.generate(counter = 1)
var code2: String = hmacOneTimePasswordGenerator.generate(counter = 2)
...
There is also a helper method isValid(code: String, counter: Int)
available in the generator's instance to validate the received code possible in one line.
The TOTP generator is available through the class TimeBasedOneTimePasswordGenerator
. Its constructor takes the shared secret and a configuration instance of the class TimeBasedOneTimePasswordConfig
as arguments:
val secret = "Leia"
val config = TimeBasedOneTimePasswordConfig(codeDigits = 8,
hmacAlgorithm = HmacAlgorithm.SHA1,
timeStep = 30,
timeStepUnit = TimeUnit.SECONDS)
val timeBasedOneTimePasswordGenerator = TimeBasedOneTimePasswordGenerator(secret.toByteArray(), config)
As well as the HOTP configuration, the TOTP configuration takes the number of code digits and the HMAC algorithm as arguments (see the previous chapter). Additionally, the time window in which the generated code is valid is represented through the arguments timeStep and time step unit. The default value of the timestamp is the current system time
The methods generate(timestamp: Long)
, generate(date: Date)
and generate(instant: Instant)
can now be used on the generator instance to generate a TOTP code:
var code0: String = timeBasedOneTimePasswordGenerator.generate() // Will use System.currentTimeMillis()
var code1: String = timeBasedOneTimePasswordGenerator.generate(timestamp = 1622234248000L)
var code2: String = timeBasedOneTimePasswordGenerator.generate(date = java.util.Date(59)) // Will internally call generate(timestamp = date.time)
var code3: String = timeBasedOneTimePasswordGenerator.generate(instant = java.time.Instant.ofEpochSecond(1622234248L)) // Will internally call generate(timestamp = instante.toEpochMillis())
...
Again, there is a helper method isValid(code: String, timestamp: Date)
available in the instance of the generator, to make the validation of the received code possible in one line.
There is also a helper method for calculating the time slot (counter) from a given timestamp, Date
, or Instant
.
var counter0: Long = timeBasedOneTimePasswordGenerator.counter() // Will use System.currentTimeMillis()
var counter1: Long = timeBasedOneTimePasswordGenerator.counter(timestamp = 1622234248000L)
var counter2: Long = timeBasedOneTimePasswordGenerator.counter(date = java.util.Date(59)) // Will internally call counter(timestamp = date.time)
var counter3: Long = timeBasedOneTimePasswordGenerator.counter(instant = java.time.Instant.ofEpochSecond(1622234248L)) // Will internally call counter(timestamp = instante.toEpochMillis())
...
You can use this counter to calculate the start and the end of a timeslot and with this how long your TOTP is still valid.
val timestamp = instant.toEpochMillis()
val totp = timeBasedOneTimePasswordGenerator.generate(timestamp)
val counter = timeBasedOneTimePasswordGenerator.counter()
val startEpochMillis = timeBasedOneTimePasswordGenerator.timeslotStart(counter)
//basically the start of next time slot minus 1ms
val endEpochMillis = timeBasedOneTimePasswordGenerator.timeslotStart(counter+1)-1
//number of milliseconds the current TOTP is still valid
val millisValid = endEpochMillis - timestamp
Some TOTP generators use the "Google way" to generate codes. This slightly different behavior means that the generator works internally with the plain text secret, but the secret gets passed around as Base32-encoded. Confusing the plain text and the Base32-encoded secret in the different steps is the most common reason people think the Google Authenticator with this library doesn't work.
The Google Authenticator generator is available through the class GoogleAuthenticator
. It is a decorator for the TOTP generator with a fixed code digits value of 6, SHA1 as HMAC algorithm, and a time window of 30 seconds. The constructor only takes the Base32-encoded secret as an argument:
// Warning: the length of the plain text may be limited, see next chapter
val plainTextSecret = "Secret1234".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().encodeToString(plainTextSecret)
println("Base32 encoded secret to be used in the Google Authenticator app: $base32EncodedSecret")
val googleAuthenticator = GoogleAuthenticator(secret = base32EncodedSecret)
var code = googleAuthenticator.generate() // Will use System.currentTimeMillis()
See the TOTP generator for the code generation generator(timestamp: Date)
and validation isValid(code: String, timestamp: Date)
methods.
There is also a helper method GoogleAuthenticator.createRandomSecretAsByteArray()
, that will return a 16-byte Base32-encoded random secret.
(Note that the Base32-encoding of the secret is just a wrapper to the outside world. Hence, the TimeBasedOneTimePasswordGenerator
internally still works with the non-encoded plain secret.)
Some generators limit the length of the plain text secret or set a fixed number of characters. So the "Google way", which has a fixed value of 10 characters. Anything outside this range will not be handled correctly by some generators.
The directory example/googleauthenticator
contains a simple JavaFX application to simulate the Google Authenticator:
Alternatively, you can use the following code to simulate the Google Authenticator on the command line. It prints a valid code for the secret K6IPBHCQTVLCZDM2
every second.
fun main() {
val base32Secret = "K6IPBHCQTVLCZDM2"
Timer().schedule(object: TimerTask() {
override fun run() {
val timestamp = Date(System.currentTimeMillis())
val code = GoogleAuthenticator(base32Secret).generate(timestamp)
println("${SimpleDateFormat("HH:mm:ss").format(timestamp)}: $code")
}
}, 0, 1000)
}
RFC 4226 recommends using a secret of the same length as the hash produced by the HMAC algorithm. The class RandomSecretGenerator
can be used to generate such random shared secrets:
val randomSecretGenerator = RandomSecretGenerator()
val secret0: ByteArray = randomSecretGenerator.createRandomSecret(HmacAlgorithm.SHA1) // 20-byte secret
val secret1: ByteArray = randomSecretGenerator.createRandomSecret(HmacAlgorithm.SHA256) // 32-byte secret
val secret2: ByteArray = randomSecretGenerator.createRandomSecret(HmacAlgorithm.SHA512) // 64-byte secret
val secret3: ByteArray = randomSecretGenerator.createRandomSecret(1234) // 1234-byte secret
The Key Uri Format specification defines a URI which can carry all generator configuration values. This URI can be embedded inside a QR code, which makes the setup of an OTP account in OTP apps easy and error-free.
This library provides the OtpAuthUriBuilder
do generate such a URI. For example:
OtpAuthUriBuilder.forTotp(Base32().encode("secret".toByteArray()))
.label("John", "Company")
.issuer("Company")
.digits(8)
.buildToString()
Would generate the URI:
otpauth://totp/Company:John/?issuer=Company&digits=8&secret=ONSWG4TFOQ
All three generators are providing the method otpAuthUriBuilder()
to create an OtpAuthUriBuilder
which already has all the configuration values set. For example:
GoogleAuthenticator(Base32().encode("secret".toByteArray()))
.otpAuthUriBuilder()
.issuer("Company")
.buildToString()
Would generate the URI:
otpauth://totp/?algorithm=SHA1&digits=6&period=30&issuer=Company&secret=ONSWG4TFOQ
Note that according to the specification, the Base32 padding character =
will be removed in the secret
parameter value (e.g., the Base32-encoded secret of foo
is MZXW6===
and would end as MZXW6
in the secret
parameter).
Copyright (c) 2023 Marcel Kliemannel
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 https://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.