Skip to content

Commit cc32917

Browse files
#5 Fix missing floor call in the calculation of the counter value of the TOTP. (#6)
Additional notable changes: - Update Gradle to 6.5.1 - Change license to Apache License, Version 2.0
1 parent 07e24b9 commit cc32917

15 files changed

+169
-99
lines changed

NOTICE

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Copyright (c) 2020 Marcel Kliemannel
2+
3+
This project is licensed under Apache License, Version 2.0;
4+
you may not use them except in compliance with the License.

README.md

+27-12
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ The library is available at [Maven Central](https://mvnrepository.com/artifact/d
2121

2222
```java
2323
// Groovy
24-
compile 'dev.turingcomplete:kotlin-onetimepassword:2.0.0'
24+
compile 'dev.turingcomplete:kotlin-onetimepassword:2.0.1'
2525

2626
// Kotlin
27-
compile("dev.turingcomplete:kotlin-onetimepassword:2.0.0")
27+
compile("dev.turingcomplete:kotlin-onetimepassword:2.0.1")
2828
```
2929

3030
### Maven
@@ -33,7 +33,7 @@ compile("dev.turingcomplete:kotlin-onetimepassword:2.0.0")
3333
<dependency>
3434
<groupId>dev.turingcomplete</groupId>
3535
<artifactId>kotlin-onetimepassword</artifactId>
36-
<version>2.0.0</version>
36+
<version>2.0.1</version>
3737
</dependency>
3838
```
3939

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

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

133+
#### Simulator Code
134+
135+
The following code can be used to simulate the Google Authenticator. It prints a valid code for the secret `K6IPBHCQTVLCZDM2` every second.
136+
```kotlin
137+
fun main() {
138+
val base64Secret = "K6IPBHCQTVLCZDM2"
139+
140+
Timer().schedule(object: TimerTask() {
141+
override fun run() {
142+
val timestamp = Date(System.currentTimeMillis())
143+
val code = GoogleAuthenticator(base64Secret).generate(timestamp)
144+
println("${SimpleDateFormat("HH:mm:ss").format(timestamp)}: $code")
145+
}
146+
}, 0, 1000)
147+
}
148+
```
149+
133150
### Random Secret Generator
134151

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

146-
## License
163+
## Licensing
164+
165+
Copyright (c) 2020 Marcel Kliemannel
166+
167+
Licensed under the **Apache License, Version 2.0** (the "License"); you may not use this file except in compliance with the License.
147168

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

150-
> Copyright 2019 Marcel Kliemannel
151-
>
152-
> 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:
153-
>
154-
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
155-
>
156-
> 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.
171+
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.

build.gradle.kts

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import java.net.URI
2+
13
plugins {
24
`java-library`
35
kotlin("jvm") version "1.3.41"
4-
id("org.jetbrains.dokka") version "0.9.18"
6+
id("org.jetbrains.dokka") version "0.10.1"
57

68
signing
79
`maven-publish`
8-
id("de.marcphilipp.nexus-publish") version "0.2.0"
910
}
1011

1112
group = "dev.turingcomplete"
12-
version = "2.0.0"
13+
version = "2.0.1"
1314

1415
tasks.withType<Wrapper> {
15-
gradleVersion = "5.5.1"
16+
gradleVersion = "6.5.1"
1617
}
1718

1819
repositories {
@@ -79,26 +80,16 @@ publishing {
7980

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

87-
gradle.taskGraph.whenReady {
88-
if (allTasks.any { it is Sign }) {
89-
extra["signing.keyId"] = ""
90-
extra["signing.password"] = ""
91-
extra["signing.secretKeyRingFile"] = ""
92-
}
93-
}
94-
95-
/**
96-
* see https://github.com/marcphilipp/nexus-publish-plugin/blob/master/README.md
97-
*/
98-
ext["serverUrl"] = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
99-
ext["nexusUsername"] = ""
100-
ext["nexusPassword"] = ""
101-
10293
configure<PublishingExtension> {
10394
publications {
10495
afterEvaluate {
@@ -116,8 +107,8 @@ configure<PublishingExtension> {
116107
}
117108
licenses {
118109
license {
119-
name.set("MIT License")
120-
url.set("https://opensource.org/licenses/MIT")
110+
name.set("The Apache Software License, Version 2.0")
111+
url.set("http://www.apache.org/licenses/LICENSE-2.0")
121112
}
122113
}
123114
issueManagement {
@@ -133,4 +124,13 @@ configure<PublishingExtension> {
133124
}
134125
}
135126
}
127+
repositories {
128+
maven {
129+
url = URI("https://oss.sonatype.org/service/local/staging/deploy/maven2")
130+
credentials {
131+
username = ""
132+
password = ""
133+
}
134+
}
135+
}
136136
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

src/main/kotlin/dev/turingcomplete/kotlinonetimepassword/GoogleAuthenticator.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import java.util.concurrent.TimeUnit
66

77
/**
88
* This class is a decorator of the [TimeBasedOneTimePasswordGenerator] that
9-
* provides the default values used by the Google Authenticator: HMAC algorithm:
10-
* SHA1; time step: 30 seconds and code digits: 6.
9+
* provides the default values used by the Google Authenticator:
10+
* - HMAC algorithm: SHA1;
11+
* - time step: 30 seconds;
12+
* - and code digits: 6.
1113
*
1214
* @param base32secret the shared secret <b>that must already be Base32-encoded</b>
1315
* (use [org.apache.commons.codec.binary.BaseNCodec.encode(byte[])]).

src/main/kotlin/dev/turingcomplete/kotlinonetimepassword/HmacOneTimePasswordConfig.kt

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package dev.turingcomplete.kotlinonetimepassword
22

3+
import java.lang.IllegalArgumentException
4+
35
/**
46
* The configuration for the [HmacOneTimePasswordGenerator].
57
*
68
* @property codeDigits the length of the generated code. The RFC 4226 requires
7-
* a code digits value between 6 and 8, to assure a good
9+
* a code digits value between 6 and 8 to assure a good
810
* security trade-off. However, this library does not set
911
* any requirement for this property. But notice that through
1012
* the design of the algorithm the maximum code value is
@@ -15,5 +17,11 @@ package dev.turingcomplete.kotlinonetimepassword
1517
* @property hmacAlgorithm the "keyed-hash message authentication code" algorithm
1618
* to use to generate the hash, from which the code is
1719
* extracted (see [HmacAlgorithm] for available algorithms).
20+
*
21+
* @throws IllegalArgumentException if `codeDigits` is negative.
1822
*/
19-
open class HmacOneTimePasswordConfig(var codeDigits: Int, var hmacAlgorithm: HmacAlgorithm)
23+
open class HmacOneTimePasswordConfig(var codeDigits: Int, var hmacAlgorithm: HmacAlgorithm) {
24+
init {
25+
require(codeDigits >= 0) { "Code digits must have a positive value." }
26+
}
27+
}

src/main/kotlin/dev/turingcomplete/kotlinonetimepassword/HmacOneTimePasswordGenerator.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import kotlin.math.pow
1616
open class HmacOneTimePasswordGenerator(private val secret: ByteArray,
1717
private val config: HmacOneTimePasswordConfig) {
1818
/**
19-
* Generated a code as a HOTP one-time password.
19+
* Generates a code representing a HMAC-based one-time password.
2020
*
2121
* @return The generated code for the provided counter value. Note, that the
2222
* code must be represented as a string because it can have trailing

src/main/kotlin/dev/turingcomplete/kotlinonetimepassword/TimeBasedOneTimePasswordConfig.kt

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.turingcomplete.kotlinonetimepassword
22

3+
import java.lang.IllegalArgumentException
34
import java.util.concurrent.TimeUnit
45

56
/**
@@ -10,8 +11,15 @@ import java.util.concurrent.TimeUnit
1011
* @property timeStepUnit see [timeStep]
1112
* @property codeDigits see documentation in [HmacOneTimePasswordConfig].
1213
* @property hmacAlgorithm see documentation in [HmacOneTimePasswordConfig].
14+
*
15+
* @throws IllegalArgumentException if `timeStep` is negative.
1316
*/
1417
open class TimeBasedOneTimePasswordConfig(val timeStep: Long,
1518
val timeStepUnit: TimeUnit,
1619
codeDigits: Int,
17-
hmacAlgorithm: HmacAlgorithm): HmacOneTimePasswordConfig(codeDigits, hmacAlgorithm)
20+
hmacAlgorithm: HmacAlgorithm): HmacOneTimePasswordConfig(codeDigits, hmacAlgorithm) {
21+
22+
init {
23+
require(timeStep >= 0) { "Time step must have a positive value." }
24+
}
25+
}

src/main/kotlin/dev/turingcomplete/kotlinonetimepassword/TimeBasedOneTimePasswordGenerator.kt

+20-7
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,42 @@ package dev.turingcomplete.kotlinonetimepassword
22

33
import java.util.*
44
import java.util.concurrent.TimeUnit
5+
import kotlin.math.floor
56

67
/**
78
* Generator for the RFC 6238 "TOTP: Time-Based One-Time Password Algorithm"
89
* (https://tools.ietf.org/html/rfc6238)
910
*
1011
* @property secret the shared secret as a byte array.
11-
* @property config the configuration for this generator.
12+
* @property config the [TimeBasedOneTimePasswordConfig] for this generator.
1213
*/
13-
open class TimeBasedOneTimePasswordGenerator(private val secret: ByteArray, private val config: TimeBasedOneTimePasswordConfig){
14+
open class TimeBasedOneTimePasswordGenerator(private val secret: ByteArray, private val config: TimeBasedOneTimePasswordConfig) {
15+
1416
private val hmacOneTimePasswordGenerator: HmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator(secret, config)
1517

1618
/**
17-
* Generated a code as a TOTP one-time password.
19+
* Generates a code representing the time-based one-time password.
1820
*
19-
* @param timestamp the challenge for the code. The default value is the
20-
* current system time from [System.currentTimeMillis].
21+
* The TOTP algorithm uses the HTOP algorithm via [HmacOneTimePasswordGenerator.generate],
22+
* with a counter parameter that represents the number of `timeStep`s from
23+
* [TimeBasedOneTimePasswordConfig] which fits into the [timestamp].
24+
*
25+
* The timestamp can be seen as the challenge to be solved. This should
26+
* normally be a continuous value over time (e.g. the current time).
27+
*
28+
* @param timestamp The Unix timestamp against the counting of the time
29+
* steps is calculated. The default value is the current system time from
30+
* [System.currentTimeMillis].
2131
*/
2232
fun generate(timestamp: Date = Date(System.currentTimeMillis())): String {
33+
2334
val counter = if (config.timeStep == 0L) {
24-
0 // To avoide a divide by zero exception
35+
0 // To avoid a divide by zero exception
2536
}
2637
else {
27-
timestamp.time.div(TimeUnit.MILLISECONDS.convert(config.timeStep, config.timeStepUnit))
38+
floor((timestamp.time).toDouble()
39+
.div(TimeUnit.MILLISECONDS.convert(config.timeStep, config.timeStepUnit).toDouble()))
40+
.toLong()
2841
}
2942

3043
return hmacOneTimePasswordGenerator.generate(counter)

src/test/kotlin/dev/turingcomplete/kotlinonetimepassword/GoogleAuthenticatorTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ class GoogleAuthenticatorTest {
1111
@ParameterizedTest(name = "Timestamp: {0}, expected code: {1}")
1212
@DisplayName("Multiple Test Vectors")
1313
@CsvFileSource(resources = ["/dev/turingcomplete/googleAuthenticatorTestVectors.csv"])
14-
fun zeroCodeDigitsTest(timestamp: Long, expectedCode: String) {
14+
fun testGeneratedCodes(timestamp: Long, expectedCode: String) {
1515
val googleAuthenticator = GoogleAuthenticator("Leia")
1616
Assertions.assertEquals(expectedCode, googleAuthenticator.generate(Date(timestamp)))
1717
Assertions.assertTrue(googleAuthenticator.isValid(expectedCode, Date(timestamp)))
1818
}
1919

2020
@Test
2121
@DisplayName("16 Bytes generated secret")
22-
fun generatedSecretExact16Bytes() {
22+
fun testGeneratedSecretToBeExactly16Bytes() {
2323
val googleAuthenticatorRandomSecret = GoogleAuthenticator.createRandomSecret()
2424
Assertions.assertEquals(16, googleAuthenticatorRandomSecret.toByteArray().size)
2525
}

src/test/kotlin/dev/turingcomplete/kotlinonetimepassword/HmacOneTimePasswordGeneratorTest.kt

+5-6
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,26 @@ import org.junit.jupiter.params.provider.CsvSource
1010
class HmacOneTimePasswordGeneratorTest {
1111
@Test
1212
@DisplayName("Edge case: 0 code digits")
13-
fun zeroCodeDigitsTest() {
13+
fun testZeroCodeDigits() {
1414
val config = HmacOneTimePasswordConfig(0, HmacAlgorithm.SHA1)
1515
val hmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator("Leia".toByteArray(), config)
1616

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

2020
@Test
21-
@DisplayName("Negative and zero counter values")
22-
fun negativeZeroAndCounterValues() {
21+
@DisplayName("Zero counter value")
22+
fun testZeroAndCounterValue() {
2323
val config = HmacOneTimePasswordConfig(8, HmacAlgorithm.SHA1)
2424
val hmacOneTimePasswordGenerator = HmacOneTimePasswordGenerator("Leia".toByteArray(), config)
2525

2626
Assertions.assertEquals("67527464", hmacOneTimePasswordGenerator.generate(0))
27-
Assertions.assertEquals("28203295", hmacOneTimePasswordGenerator.generate(-42))
2827
}
2928

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

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

src/test/kotlin/dev/turingcomplete/kotlinonetimepassword/RandomSecretGeneratorTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test
1010
class RandomSecretGeneratorTest {
1111
@Test
1212
@DisplayName("Same secret length as the HMAC algorithm hash")
13-
fun expectedHmacAlgorithmHashLength() {
13+
fun testExpectedHmacAlgorithmHashLength() {
1414
HmacAlgorithm.values().forEach {
1515
val randomSecret = RandomSecretGenerator().createRandomSecret(it)
1616
Assertions.assertEquals(it.hashBytes, randomSecret.size)

0 commit comments

Comments
 (0)