Skip to content

Commit

Permalink
#5 Fix missing floor call in the calculation of the counter value of …
Browse files Browse the repository at this point in the history
…the TOTP. (#6)

Additional notable changes:
- Update Gradle to 6.5.1
- Change license to Apache License, Version 2.0
  • Loading branch information
marcelkliemannel authored Jul 3, 2020
1 parent 07e24b9 commit cc32917
Show file tree
Hide file tree
Showing 15 changed files with 169 additions and 99 deletions.
4 changes: 4 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Copyright (c) 2020 Marcel Kliemannel

This project is licensed under Apache License, Version 2.0;
you may not use them except in compliance with the License.
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ The library is available at [Maven Central](https://mvnrepository.com/artifact/d

```java
// Groovy
compile 'dev.turingcomplete:kotlin-onetimepassword:2.0.0'
compile 'dev.turingcomplete:kotlin-onetimepassword:2.0.1'

// Kotlin
compile("dev.turingcomplete:kotlin-onetimepassword:2.0.0")
compile("dev.turingcomplete:kotlin-onetimepassword:2.0.1")
```

### Maven
Expand All @@ -33,7 +33,7 @@ compile("dev.turingcomplete:kotlin-onetimepassword:2.0.0")
<dependency>
<groupId>dev.turingcomplete</groupId>
<artifactId>kotlin-onetimepassword</artifactId>
<version>2.0.0</version>
<version>2.0.1</version>
</dependency>
```

Expand Down Expand Up @@ -130,6 +130,23 @@ See the TOTP generator for the code generation ```generator(timestamp: Date)```

There is also a helper method ```GoogleAuthenticator.createRandomSecret()``` that will return a 16-byte Base32-decoded random secret.

#### Simulator Code

The following code can be used to simulate the Google Authenticator. It prints a valid code for the secret `K6IPBHCQTVLCZDM2` every second.
```kotlin
fun main() {
val base64Secret = "K6IPBHCQTVLCZDM2"

Timer().schedule(object: TimerTask() {
override fun run() {
val timestamp = Date(System.currentTimeMillis())
val code = GoogleAuthenticator(base64Secret).generate(timestamp)
println("${SimpleDateFormat("HH:mm:ss").format(timestamp)}: $code")
}
}, 0, 1000)
}
```

### Random Secret Generator

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:
Expand All @@ -143,14 +160,12 @@ val secret2: ByteArray = randomSecretGenerator.createRandomSecret(HmacAlgorithm.
val secret3: ByteArray = randomSecretGenerator.createRandomSecret(1234) // 1234-byte secret
```

## License
## Licensing

Copyright (c) 2020 Marcel Kliemannel

Licensed under the **Apache License, Version 2.0** (the "License"); you may not use this file except in compliance with the License.

**MIT License**
You may obtain a copy of the License at <https://www.apache.org/licenses/LICENSE-2.0>.

> Copyright 2019 Marcel Kliemannel
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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](./LICENSE) for the specific language governing permissions and limitations under the License.
42 changes: 21 additions & 21 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import java.net.URI

plugins {
`java-library`
kotlin("jvm") version "1.3.41"
id("org.jetbrains.dokka") version "0.9.18"
id("org.jetbrains.dokka") version "0.10.1"

signing
`maven-publish`
id("de.marcphilipp.nexus-publish") version "0.2.0"
}

group = "dev.turingcomplete"
version = "2.0.0"
version = "2.0.1"

tasks.withType<Wrapper> {
gradleVersion = "5.5.1"
gradleVersion = "6.5.1"
}

repositories {
Expand Down Expand Up @@ -79,26 +80,16 @@ publishing {

/**
* See https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials
*
* The following Gradle properties must be set:
* - signing.keyId (last 8 symbols of the key ID from 'gpg -K')
* - signing.password
* - signing.secretKeyRingFile ('gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg')
*/
signing {
sign(publishing.publications[project.name])
}

gradle.taskGraph.whenReady {
if (allTasks.any { it is Sign }) {
extra["signing.keyId"] = ""
extra["signing.password"] = ""
extra["signing.secretKeyRingFile"] = ""
}
}

/**
* see https://github.com/marcphilipp/nexus-publish-plugin/blob/master/README.md
*/
ext["serverUrl"] = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
ext["nexusUsername"] = ""
ext["nexusPassword"] = ""

configure<PublishingExtension> {
publications {
afterEvaluate {
Expand All @@ -116,8 +107,8 @@ configure<PublishingExtension> {
}
licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
name.set("The Apache Software License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0")
}
}
issueManagement {
Expand All @@ -133,4 +124,13 @@ configure<PublishingExtension> {
}
}
}
repositories {
maven {
url = URI("https://oss.sonatype.org/service/local/staging/deploy/maven2")
credentials {
username = ""
password = ""
}
}
}
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import java.util.concurrent.TimeUnit

/**
* This class is a decorator of the [TimeBasedOneTimePasswordGenerator] that
* provides the default values used by the Google Authenticator: HMAC algorithm:
* SHA1; time step: 30 seconds and code digits: 6.
* provides the default values used by the Google Authenticator:
* - HMAC algorithm: SHA1;
* - time step: 30 seconds;
* - and code digits: 6.
*
* @param base32secret the shared secret <b>that must already be Base32-encoded</b>
* (use [org.apache.commons.codec.binary.BaseNCodec.encode(byte[])]).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package dev.turingcomplete.kotlinonetimepassword

import java.lang.IllegalArgumentException

/**
* The configuration for the [HmacOneTimePasswordGenerator].
*
* @property codeDigits the length of the generated code. The RFC 4226 requires
* a code digits value between 6 and 8, to assure a good
* a code digits value between 6 and 8 to assure a good
* security trade-off. However, this library does not set
* any requirement for this property. But notice that through
* the design of the algorithm the maximum code value is
Expand All @@ -15,5 +17,11 @@ package dev.turingcomplete.kotlinonetimepassword
* @property hmacAlgorithm the "keyed-hash message authentication code" algorithm
* to use to generate the hash, from which the code is
* extracted (see [HmacAlgorithm] for available algorithms).
*
* @throws IllegalArgumentException if `codeDigits` is negative.
*/
open class HmacOneTimePasswordConfig(var codeDigits: Int, var hmacAlgorithm: HmacAlgorithm)
open class HmacOneTimePasswordConfig(var codeDigits: Int, var hmacAlgorithm: HmacAlgorithm) {
init {
require(codeDigits >= 0) { "Code digits must have a positive value." }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import kotlin.math.pow
open class HmacOneTimePasswordGenerator(private val secret: ByteArray,
private val config: HmacOneTimePasswordConfig) {
/**
* Generated a code as a HOTP one-time password.
* Generates a code representing a HMAC-based one-time password.
*
* @return The generated code for the provided counter value. Note, that the
* code must be represented as a string because it can have trailing
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.turingcomplete.kotlinonetimepassword

import java.lang.IllegalArgumentException
import java.util.concurrent.TimeUnit

/**
Expand All @@ -10,8 +11,15 @@ import java.util.concurrent.TimeUnit
* @property timeStepUnit see [timeStep]
* @property codeDigits see documentation in [HmacOneTimePasswordConfig].
* @property hmacAlgorithm see documentation in [HmacOneTimePasswordConfig].
*
* @throws IllegalArgumentException if `timeStep` is negative.
*/
open class TimeBasedOneTimePasswordConfig(val timeStep: Long,
val timeStepUnit: TimeUnit,
codeDigits: Int,
hmacAlgorithm: HmacAlgorithm): HmacOneTimePasswordConfig(codeDigits, hmacAlgorithm)
hmacAlgorithm: HmacAlgorithm): HmacOneTimePasswordConfig(codeDigits, hmacAlgorithm) {

init {
require(timeStep >= 0) { "Time step must have a positive value." }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,42 @@ package dev.turingcomplete.kotlinonetimepassword

import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.floor

/**
* Generator for the RFC 6238 "TOTP: Time-Based One-Time Password Algorithm"
* (https://tools.ietf.org/html/rfc6238)
*
* @property secret the shared secret as a byte array.
* @property config the configuration for this generator.
* @property config the [TimeBasedOneTimePasswordConfig] for this generator.
*/
open class TimeBasedOneTimePasswordGenerator(private val secret: ByteArray, private val config: TimeBasedOneTimePasswordConfig){
open class TimeBasedOneTimePasswordGenerator(private val secret: ByteArray, private val config: TimeBasedOneTimePasswordConfig) {

private val hmacOneTimePasswordGenerator: HmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator(secret, config)

/**
* Generated a code as a TOTP one-time password.
* Generates a code representing the time-based one-time password.
*
* @param timestamp the challenge for the code. The default value is the
* current system time from [System.currentTimeMillis].
* The TOTP algorithm uses the HTOP algorithm via [HmacOneTimePasswordGenerator.generate],
* with a counter parameter that represents the number of `timeStep`s from
* [TimeBasedOneTimePasswordConfig] which fits into the [timestamp].
*
* The timestamp can be seen as the challenge to be solved. This should
* normally be a continuous value over time (e.g. the current time).
*
* @param timestamp The Unix timestamp against the counting of the time
* steps is calculated. The default value is the current system time from
* [System.currentTimeMillis].
*/
fun generate(timestamp: Date = Date(System.currentTimeMillis())): String {

val counter = if (config.timeStep == 0L) {
0 // To avoide a divide by zero exception
0 // To avoid a divide by zero exception
}
else {
timestamp.time.div(TimeUnit.MILLISECONDS.convert(config.timeStep, config.timeStepUnit))
floor((timestamp.time).toDouble()
.div(TimeUnit.MILLISECONDS.convert(config.timeStep, config.timeStepUnit).toDouble()))
.toLong()
}

return hmacOneTimePasswordGenerator.generate(counter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ class GoogleAuthenticatorTest {
@ParameterizedTest(name = "Timestamp: {0}, expected code: {1}")
@DisplayName("Multiple Test Vectors")
@CsvFileSource(resources = ["/dev/turingcomplete/googleAuthenticatorTestVectors.csv"])
fun zeroCodeDigitsTest(timestamp: Long, expectedCode: String) {
fun testGeneratedCodes(timestamp: Long, expectedCode: String) {
val googleAuthenticator = GoogleAuthenticator("Leia")
Assertions.assertEquals(expectedCode, googleAuthenticator.generate(Date(timestamp)))
Assertions.assertTrue(googleAuthenticator.isValid(expectedCode, Date(timestamp)))
}

@Test
@DisplayName("16 Bytes generated secret")
fun generatedSecretExact16Bytes() {
fun testGeneratedSecretToBeExactly16Bytes() {
val googleAuthenticatorRandomSecret = GoogleAuthenticator.createRandomSecret()
Assertions.assertEquals(16, googleAuthenticatorRandomSecret.toByteArray().size)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,26 @@ import org.junit.jupiter.params.provider.CsvSource
class HmacOneTimePasswordGeneratorTest {
@Test
@DisplayName("Edge case: 0 code digits")
fun zeroCodeDigitsTest() {
fun testZeroCodeDigits() {
val config = HmacOneTimePasswordConfig(0, HmacAlgorithm.SHA1)
val hmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator("Leia".toByteArray(), config)

Assertions.assertEquals(0, hmacOneTimePasswordGenerator.generate(42).length)
}

@Test
@DisplayName("Negative and zero counter values")
fun negativeZeroAndCounterValues() {
@DisplayName("Zero counter value")
fun testZeroAndCounterValue() {
val config = HmacOneTimePasswordConfig(8, HmacAlgorithm.SHA1)
val hmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator("Leia".toByteArray(), config)

Assertions.assertEquals("67527464", hmacOneTimePasswordGenerator.generate(0))
Assertions.assertEquals("28203295", hmacOneTimePasswordGenerator.generate(-42))
}

@ParameterizedTest(name = "{0}, code digits: {1}, counter: {2}, code: {3}, secret: {4}")
@DisplayName("Multiple algorithms, code digits, counter and secrets")
@CsvFileSource(resources = ["/dev/turingcomplete/multipleHmacOneTimePasswordTestVectors.csv"])
fun multipleTestVectors(hmacAlgorithm: String, codeDigits: Int, counter: Long, expectedCode: String, secret: String) {
fun testGeneratedCodes(hmacAlgorithm: String, codeDigits: Int, counter: Long, expectedCode: String, secret: String) {
validateWithExpectedCode(counter, expectedCode, codeDigits, secret, HmacAlgorithm.valueOf(hmacAlgorithm))
}

Expand All @@ -40,7 +39,7 @@ class HmacOneTimePasswordGeneratorTest {
"0, 755224", "1, 287082", "2, 359152", "3, 969429", "4, 338314",
"5, 254676", "6, 287922", "7, 162583", "8, 399871", "9, 520489"
])
fun rfc4226AppendixDTestCases(counter: Long, code: String) {
fun testRfc4226AppendixDTestCases(counter: Long, code: String) {
validateWithExpectedCode(counter, code, 6, "12345678901234567890", HmacAlgorithm.SHA1)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test
class RandomSecretGeneratorTest {
@Test
@DisplayName("Same secret length as the HMAC algorithm hash")
fun expectedHmacAlgorithmHashLength() {
fun testExpectedHmacAlgorithmHashLength() {
HmacAlgorithm.values().forEach {
val randomSecret = RandomSecretGenerator().createRandomSecret(it)
Assertions.assertEquals(it.hashBytes, randomSecret.size)
Expand Down
Loading

0 comments on commit cc32917

Please sign in to comment.