├── .gitignore
├── .github
└── workflows
│ └── test.yml
├── LICENSE
├── src
├── assembly
│ └── benchmark.xml
├── main
│ └── java
│ │ ├── com
│ │ └── eatthepath
│ │ │ └── otp
│ │ │ ├── UncheckedNoSuchAlgorithmException.java
│ │ │ ├── package-info.java
│ │ │ ├── TimeBasedOneTimePasswordGenerator.java
│ │ │ └── HmacOneTimePasswordGenerator.java
│ │ └── overview.html
├── benchmark
│ └── java
│ │ └── com
│ │ └── eatthepath
│ │ └── otp
│ │ └── HmacOneTimePasswordGeneratorBenchmark.java
└── test
│ └── java
│ └── com
│ └── eatthepath
│ └── otp
│ ├── ExampleApp.java
│ ├── HmacOneTimePasswordGeneratorTest.java
│ └── TimeBasedOneTimePasswordGeneratorTest.java
├── README.md
└── pom.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | .DS_Store
3 | doc
4 | .classpath
5 | .project
6 | .settings
7 | _site
8 | #ignore intellij files
9 | *.iml
10 | .idea/
11 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Build/test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | distribution: [ 'zulu', 'temurin' ]
11 | java: [8, 11, 17, 18]
12 | fail-fast: false
13 | name: JDK ${{ matrix.java }} (${{ matrix.distribution }})
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up JDK
18 | uses: actions/setup-java@v2
19 | with:
20 | distribution: ${{ matrix.distribution }}
21 | java-version: ${{ matrix.java }}
22 | cache: 'maven'
23 | - name: Test with Maven
24 | run: mvn verify -B --file pom.xml
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Jon Chambers
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
--------------------------------------------------------------------------------
/src/assembly/benchmark.xml:
--------------------------------------------------------------------------------
1 |
3 | benchmark
4 |
5 | jar
6 |
7 | false
8 |
9 |
10 | /
11 | true
12 | true
13 | test
14 |
15 |
16 |
17 |
18 | ${project.build.directory}/test-classes
19 | /
20 |
21 | **/*
22 |
23 | true
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/main/java/com/eatthepath/otp/UncheckedNoSuchAlgorithmException.java:
--------------------------------------------------------------------------------
1 | package com.eatthepath.otp;
2 |
3 | import java.security.NoSuchAlgorithmException;
4 |
5 | /**
6 | * Wraps a {@link NoSuchAlgorithmException} with an unchecked exception.
7 | *
8 | * @author Jon Chambers
9 | */
10 | public class UncheckedNoSuchAlgorithmException extends RuntimeException {
11 |
12 | /**
13 | * Constructs a new unchecked {@code NoSuchAlgorithmException} instance.
14 | *
15 | * @param cause the underlying {@code NoSuchAlgorithmException}
16 | */
17 | UncheckedNoSuchAlgorithmException(final NoSuchAlgorithmException cause) {
18 | super(cause);
19 | }
20 |
21 | /**
22 | * Returns the underlying {@link NoSuchAlgorithmException} that caused this exception.
23 | *
24 | * @return the underlying {@link NoSuchAlgorithmException} that caused this exception
25 | */
26 | @Override
27 | public NoSuchAlgorithmException getCause() {
28 | return (NoSuchAlgorithmException) super.getCause();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/benchmark/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorBenchmark.java:
--------------------------------------------------------------------------------
1 | package com.eatthepath.otp;
2 |
3 | import org.openjdk.jmh.annotations.Benchmark;
4 | import org.openjdk.jmh.annotations.Scope;
5 | import org.openjdk.jmh.annotations.Setup;
6 | import org.openjdk.jmh.annotations.State;
7 |
8 | import javax.crypto.KeyGenerator;
9 | import java.security.InvalidKeyException;
10 | import java.security.Key;
11 | import java.security.NoSuchAlgorithmException;
12 |
13 | @State(Scope.Benchmark)
14 | public class HmacOneTimePasswordGeneratorBenchmark {
15 |
16 | private HmacOneTimePasswordGenerator hotp;
17 | private Key key;
18 |
19 | private int counter = 0;
20 |
21 | @Setup
22 | public void setUp() throws NoSuchAlgorithmException {
23 | this.hotp = new HmacOneTimePasswordGenerator();
24 |
25 | final KeyGenerator keyGenerator = KeyGenerator.getInstance(this.hotp.getAlgorithm());
26 | keyGenerator.init(512);
27 |
28 | this.key = keyGenerator.generateKey();
29 | }
30 |
31 | @Benchmark
32 | public int benchmarkGenerateOneTimePassword() throws InvalidKeyException {
33 | return this.hotp.generateOneTimePassword(this.key, this.counter++);
34 | }
35 |
36 | @Benchmark
37 | public String benchmarkGenerateOneTimePasswordString() throws InvalidKeyException {
38 | return this.hotp.generateOneTimePasswordString(this.key, this.counter++);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/eatthepath/otp/package-info.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2016 Jon Chambers
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE. */
20 |
21 | /**
22 | * Contains classes for generating one-time passwords using either the
23 | * HOTP (RFC 4226) or
24 | * TOTP (RFC 6328) algorithms.
25 | *
26 | * @author Jon Chambers
27 | */
28 | package com.eatthepath.otp;
29 |
--------------------------------------------------------------------------------
/src/test/java/com/eatthepath/otp/ExampleApp.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2016 Jon Chambers
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE. */
20 |
21 | package com.eatthepath.otp;
22 |
23 | import javax.crypto.KeyGenerator;
24 | import javax.crypto.Mac;
25 | import java.security.InvalidKeyException;
26 | import java.security.Key;
27 | import java.security.NoSuchAlgorithmException;
28 | import java.time.Instant;
29 |
30 | public class ExampleApp {
31 | public static void main(final String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
32 | final TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator();
33 |
34 | final Key key;
35 | {
36 | final KeyGenerator keyGenerator = KeyGenerator.getInstance(totp.getAlgorithm());
37 |
38 | // Key length should match the length of the HMAC output (160 bits for SHA-1, 256 bits
39 | // for SHA-256, and 512 bits for SHA-512). Note that while Mac#getMacLength() returns a
40 | // length in _bytes,_ KeyGenerator#init(int) takes a key length in _bits._
41 | final int macLengthInBytes = Mac.getInstance(totp.getAlgorithm()).getMacLength();
42 | keyGenerator.init(macLengthInBytes * 8);
43 |
44 | key = keyGenerator.generateKey();
45 | }
46 |
47 | final Instant now = Instant.now();
48 | final Instant later = now.plus(totp.getTimeStep());
49 |
50 | System.out.println("Current password: " + totp.generateOneTimePasswordString(key, now));
51 | System.out.println("Future password: " + totp.generateOneTimePasswordString(key, later));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/overview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
24 |
To demonstrate generating one-time passwords, we'll focus on the TOTP algorithm. To create a TOTP generator with a default password length, time step, and HMAC algorithm:
34 |
35 |
final TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator();
36 |
37 |
To actually generate time-based one-time passwords, you'll need a secret key and a timestamp. Secure key management is beyond the scope of this document; for the purposes of an example, though, we'll generate a random key:
Armed with a secret key, we can deterministically generate one-time passwords for any timestamp:
48 |
49 |
final Instant now = Instant.now();
50 | final Instant later = now.plus(totp.getTimeStep());
51 |
52 | System.out.println("Current password: " + totp.generateOneTimePasswordString(key, now));
53 | System.out.println("Future password: " + totp.generateOneTimePasswordString(key, later));
54 |
55 |
License and copyright
56 |
57 |
java-otp is copyright (c) 2016 Jon Chambers and available under the MIT License.
58 |
59 |
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/jchambers/java-otp)
2 | [](https://maven-badges.herokuapp.com/maven-central/com.eatthepath/java-otp)
3 |
4 | java-otp is a Java library for generating [HOTP (RFC 4226)](https://tools.ietf.org/html/rfc4226) or [TOTP (RFC 6238)](https://tools.ietf.org/html/rfc6238) one-time passwords.
5 |
6 | ## Getting java-otp
7 |
8 | You can download java-otp as a jar file (it has no dependencies) from the [GitHub releases page](https://github.com/jchambers/java-otp/releases) and add it to your project's classpath. If you're using Maven (or something that understands Maven dependencies) to build your project, you can add java-otp as a dependency:
9 |
10 | ```xml
11 |
12 | com.eatthepath
13 | java-otp
14 | 0.4.0
15 |
16 | ```
17 |
18 | java-otp works with Java 8 or newer. If you need support for versions of Java older than Java 8, you may try using [java-otp v0.1](https://github.com/jchambers/java-otp/releases/tag/java-otp-0.1.0) (although it is no longer supported).
19 |
20 | ## Documentation
21 |
22 | The latest API docs are available at https://jchambers.github.io/java-otp/apidocs/latest/.
23 |
24 | ## Usage
25 |
26 | To demonstrate generating one-time passwords, we'll focus on the TOTP algorithm. To create a TOTP generator with a default password length (6 digits), time step (30 seconds), and HMAC algorithm (HMAC-SHA1):
27 |
28 | ```java
29 | final TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator();
30 | ```
31 |
32 | To actually generate time-based one-time passwords, you'll need a key and a timestamp. Secure key management is beyond the scope of this document; for the purposes of an example, though, we'll generate a random key:
33 |
34 | ```java
35 | final Key key;
36 | {
37 | final KeyGenerator keyGenerator = KeyGenerator.getInstance(totp.getAlgorithm());
38 |
39 | // Key length should match the length of the HMAC output (160 bits for SHA-1, 256 bits
40 | // for SHA-256, and 512 bits for SHA-512). Note that while Mac#getMacLength() returns a
41 | // length in _bytes,_ KeyGenerator#init(int) takes a key length in _bits._
42 | final int macLengthInBytes = Mac.getInstance(totp.getAlgorithm()).getMacLength();
43 | keyGenerator.init(macLengthInBytes * 8);
44 |
45 | key = keyGenerator.generateKey();
46 | }
47 | ```
48 |
49 | Armed with a key, we can deterministically generate one-time passwords for any timestamp:
50 |
51 | ```java
52 | final Instant now = Instant.now();
53 | final Instant later = now.plus(totp.getTimeStep());
54 |
55 | System.out.println("Current password: " + totp.generateOneTimePasswordString(key, now));
56 | System.out.println("Future password: " + totp.generateOneTimePasswordString(key, later));
57 | ```
58 |
59 | …which produces (for one randomly-generated key):
60 |
61 | ```
62 | Current password: 164092
63 | Future password: 046148
64 | ```
65 |
66 | ## Performance and best practices
67 |
68 | One-time password generators are thread-safe and reusable. Generally, applications should treat one-time password generator instances as long-lived resources (as opposed to creating new generators for each password-generation call).
69 |
70 | ## License and copyright
71 |
72 | java-otp is published under the [MIT License](https://opensource.org/licenses/MIT).
73 |
--------------------------------------------------------------------------------
/src/test/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorTest.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2016 Jon Chambers
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE. */
20 |
21 | package com.eatthepath.otp;
22 |
23 | import org.junit.jupiter.api.Test;
24 | import org.junit.jupiter.params.ParameterizedTest;
25 | import org.junit.jupiter.params.provider.Arguments;
26 | import org.junit.jupiter.params.provider.MethodSource;
27 |
28 | import javax.crypto.spec.SecretKeySpec;
29 | import java.nio.charset.StandardCharsets;
30 | import java.security.InvalidKeyException;
31 | import java.security.Key;
32 | import java.util.Locale;
33 | import java.util.Random;
34 | import java.util.concurrent.*;
35 | import java.util.stream.Stream;
36 |
37 | import static org.junit.jupiter.api.Assertions.*;
38 | import static org.junit.jupiter.params.provider.Arguments.arguments;
39 |
40 | public class HmacOneTimePasswordGeneratorTest {
41 |
42 | private static final Key HOTP_KEY =
43 | new SecretKeySpec("12345678901234567890".getBytes(StandardCharsets.US_ASCII),
44 | HmacOneTimePasswordGenerator.HOTP_HMAC_ALGORITHM);
45 |
46 | private static final int[] TEST_VECTORS = new int[] {
47 | 755224,
48 | 287082,
49 | 359152,
50 | 969429,
51 | 338314,
52 | 254676,
53 | 287922,
54 | 162583,
55 | 399871,
56 | 520489
57 | };
58 |
59 | @Test
60 | void testHmacOneTimePasswordGeneratorWithShortPasswordLength() {
61 | assertThrows(IllegalArgumentException.class, () -> new HmacOneTimePasswordGenerator(5));
62 | }
63 |
64 | @Test
65 | void testHmacOneTimePasswordGeneratorWithLongPasswordLength() {
66 | assertThrows(IllegalArgumentException.class, () -> new HmacOneTimePasswordGenerator(9));
67 | }
68 |
69 | @Test
70 | void testHmacOneTimePasswordGeneratorWithBogusAlgorithm() {
71 | final UncheckedNoSuchAlgorithmException exception = assertThrows(UncheckedNoSuchAlgorithmException.class, () ->
72 | new HmacOneTimePasswordGenerator(6, "Definitely not a real algorithm"));
73 |
74 | assertNotNull(exception.getCause());
75 | }
76 |
77 | @Test
78 | void testGetPasswordLength() {
79 | final int passwordLength = 7;
80 | assertEquals(passwordLength, new HmacOneTimePasswordGenerator(passwordLength).getPasswordLength());
81 | }
82 |
83 | @Test
84 | void testGetAlgorithm() {
85 | final String algorithm = "HmacSHA256";
86 | assertEquals(algorithm, new HmacOneTimePasswordGenerator(6, algorithm).getAlgorithm());
87 | }
88 |
89 | /**
90 | * Tests generation of one-time passwords using the test vectors from
91 | * RFC 4226, Appendix D.
92 | */
93 | @ParameterizedTest
94 | @MethodSource("argumentsForTestGenerateOneTimePasswordHotp")
95 | void testGenerateOneTimePassword(final int counter, final int expectedOneTimePassword) throws Exception {
96 | assertEquals(expectedOneTimePassword, new HmacOneTimePasswordGenerator().generateOneTimePassword(HOTP_KEY, counter));
97 | }
98 |
99 | @Test
100 | void testGenerateOneTimePasswordRepeated() throws Exception {
101 | final HmacOneTimePasswordGenerator hotpGenerator = new HmacOneTimePasswordGenerator();
102 |
103 | for (int counter = 0; counter < TEST_VECTORS.length; counter++) {
104 | assertEquals(TEST_VECTORS[counter], hotpGenerator.generateOneTimePassword(HOTP_KEY, counter));
105 | }
106 | }
107 |
108 | private static Stream argumentsForTestGenerateOneTimePasswordHotp() {
109 | final Stream.Builder streamBuilder = Stream.builder();
110 |
111 | for (int counter = 0; counter < TEST_VECTORS.length; counter++) {
112 | streamBuilder.add(Arguments.of(counter, TEST_VECTORS[counter]));
113 | }
114 |
115 | return streamBuilder.build();
116 | }
117 |
118 | @ParameterizedTest
119 | @MethodSource("argumentsForTestGenerateOneTimePasswordStringHotp")
120 | void testGenerateOneTimePasswordString(final int counter, final String expectedOneTimePassword) throws Exception {
121 | Locale.setDefault(Locale.US);
122 | assertEquals(expectedOneTimePassword, new HmacOneTimePasswordGenerator().generateOneTimePasswordString(HOTP_KEY, counter));
123 | }
124 |
125 | private static Stream argumentsForTestGenerateOneTimePasswordStringHotp() {
126 | final Stream.Builder streamBuilder = Stream.builder();
127 |
128 | for (int counter = 0; counter < TEST_VECTORS.length; counter++) {
129 | streamBuilder.add(Arguments.of(counter, String.valueOf(TEST_VECTORS[counter])));
130 | }
131 |
132 | return streamBuilder.build();
133 | }
134 |
135 | @ParameterizedTest
136 | @MethodSource("argumentsForTestGenerateOneTimePasswordStringLocaleHotp")
137 | void testGenerateOneTimePasswordStringLocale(final int counter, final Locale locale, final String expectedOneTimePassword) throws Exception {
138 | Locale.setDefault(Locale.US);
139 | assertEquals(expectedOneTimePassword, new HmacOneTimePasswordGenerator().generateOneTimePasswordString(HOTP_KEY, counter, locale));
140 | }
141 |
142 | private static Stream argumentsForTestGenerateOneTimePasswordStringLocaleHotp() {
143 | final Locale locale = Locale.forLanguageTag("hi-IN-u-nu-Deva");
144 |
145 | return Stream.of(
146 | arguments(0, locale, "७५५२२४"),
147 | arguments(1, locale, "२८७०८२"),
148 | arguments(2, locale, "३५९१५२"),
149 | arguments(3, locale, "९६९४२९"),
150 | arguments(4, locale, "३३८३१४"),
151 | arguments(5, locale, "२५४६७६"),
152 | arguments(6, locale, "२८७९२२"),
153 | arguments(7, locale, "१६२५८३"),
154 | arguments(8, locale, "३९९८७१"),
155 | arguments(9, locale, "५२०४८९")
156 | );
157 | }
158 |
159 | @Test
160 | void testConcurrent() throws InterruptedException {
161 | final int iterations = 10_000;
162 | final int threadCount = Runtime.getRuntime().availableProcessors() * 4;
163 | final CountDownLatch threadStartLatch = new CountDownLatch(threadCount);
164 | final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
165 | @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[threadCount];
166 |
167 | final HmacOneTimePasswordGenerator hotpGenerator = new HmacOneTimePasswordGenerator();
168 |
169 | for (int thread = 0; thread < threadCount; thread++) {
170 | futures[thread] = CompletableFuture.supplyAsync(() -> {
171 | final Random random = new Random();
172 | boolean allMatched = true;
173 |
174 | threadStartLatch.countDown();
175 |
176 | try {
177 | threadStartLatch.await();
178 | } catch (final InterruptedException e) {
179 | throw new RuntimeException(e);
180 | }
181 |
182 | for (int i = 0; i < iterations; i++) {
183 | final int counter = random.nextInt(TEST_VECTORS.length);
184 |
185 | try {
186 | if (hotpGenerator.generateOneTimePassword(HOTP_KEY, counter) != TEST_VECTORS[counter]) {
187 | allMatched = false;
188 | }
189 | } catch (final InvalidKeyException e) {
190 | throw new RuntimeException(e);
191 | }
192 | }
193 |
194 | return allMatched;
195 | }, executorService);
196 | }
197 |
198 | CompletableFuture.allOf(futures).join();
199 |
200 | for (final CompletableFuture future : futures) {
201 | assertTrue(future.join());
202 | }
203 |
204 | executorService.shutdown();
205 | assertTrue(executorService.awaitTermination(1, TimeUnit.SECONDS));
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2016 Jon Chambers
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE. */
20 |
21 | package com.eatthepath.otp;
22 |
23 | import javax.crypto.Mac;
24 | import java.security.InvalidKeyException;
25 | import java.security.Key;
26 | import java.time.Duration;
27 | import java.time.Instant;
28 | import java.util.Locale;
29 |
30 | /**
31 | *
Generates time-based one-time passwords (TOTP) as specified in
32 | * RFC 6238.
33 | *
34 | *
{@code TimeBasedOneTimePasswordGenerator} instances are thread-safe and may be shared between threads. Note that
35 | * the {@link #generateOneTimePassword(Key, Instant)} method (and its relatives) are {@code synchronized}; in
36 | * multi-threaded applications that make heavy use of a shared {@code TimeBasedOneTimePasswordGenerator} instance,
37 | * synchronization may become a performance bottleneck. In that case, callers may benefit from using one
38 | * {@code TimeBasedOneTimePasswordGenerator} instance per thread (for example, with a {@link ThreadLocal}).
39 | *
40 | * @author Jon Chambers
41 | */
42 | public class TimeBasedOneTimePasswordGenerator {
43 | private final HmacOneTimePasswordGenerator hotp;
44 | private final Duration timeStep;
45 |
46 | /**
47 | * The default time-step for a time-based one-time password generator (30 seconds).
48 | */
49 | public static final Duration DEFAULT_TIME_STEP = Duration.ofSeconds(30);
50 |
51 | /**
52 | * A string identifier for the HMAC-SHA1 algorithm; HMAC-SHA1 is the default algorithm for TOTP.
53 | */
54 | public static final String TOTP_ALGORITHM_HMAC_SHA1 = "HmacSHA1";
55 |
56 | /**
57 | * A string identifier for the HMAC-SHA256 algorithm.
58 | */
59 | @SuppressWarnings("unused")
60 | public static final String TOTP_ALGORITHM_HMAC_SHA256 = "HmacSHA256";
61 |
62 | /**
63 | * A string identifier for the HMAC-SHA512 algorithm.
64 | */
65 | @SuppressWarnings("unused")
66 | public static final String TOTP_ALGORITHM_HMAC_SHA512 = "HmacSHA512";
67 |
68 | /**
69 | * Constructs a new time-based one-time password generator with a default time-step (30 seconds), password length
70 | * ({@value com.eatthepath.otp.HmacOneTimePasswordGenerator#DEFAULT_PASSWORD_LENGTH} decimal digits), and HMAC
71 | * algorithm ({@value com.eatthepath.otp.HmacOneTimePasswordGenerator#HOTP_HMAC_ALGORITHM}).
72 | */
73 | public TimeBasedOneTimePasswordGenerator() {
74 | this(DEFAULT_TIME_STEP);
75 | }
76 |
77 | /**
78 | * Constructs a new time-based one-time password generator with the given time-step and a default password length
79 | * ({@value com.eatthepath.otp.HmacOneTimePasswordGenerator#DEFAULT_PASSWORD_LENGTH} decimal digits) and HMAC
80 | * algorithm ({@value com.eatthepath.otp.HmacOneTimePasswordGenerator#HOTP_HMAC_ALGORITHM}).
81 | *
82 | * @param timeStep the time-step for this generator
83 | */
84 | public TimeBasedOneTimePasswordGenerator(final Duration timeStep) {
85 | this(timeStep, HmacOneTimePasswordGenerator.DEFAULT_PASSWORD_LENGTH);
86 | }
87 |
88 | /**
89 | * Constructs a new time-based one-time password generator with the given time-step and password length and a
90 | * default HMAC algorithm ({@value com.eatthepath.otp.HmacOneTimePasswordGenerator#HOTP_HMAC_ALGORITHM}).
91 | *
92 | * @param timeStep the time-step for this generator
93 | * @param passwordLength the length, in decimal digits, of the one-time passwords to be generated; must be between
94 | * 6 and 8, inclusive
95 | */
96 | public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int passwordLength) {
97 | this(timeStep, passwordLength, TOTP_ALGORITHM_HMAC_SHA1);
98 | }
99 |
100 | /**
101 | * Constructs a new time-based one-time password generator with the given time-step, password length, and HMAC
102 | * algorithm.
103 | *
104 | * @param timeStep the time-step for this generator
105 | * @param passwordLength the length, in decimal digits, of the one-time passwords to be generated; must be between
106 | * 6 and 8, inclusive
107 | * @param algorithm the name of the {@link javax.crypto.Mac} algorithm to use when generating passwords; TOTP allows
108 | * for {@value #TOTP_ALGORITHM_HMAC_SHA1}, {@value #TOTP_ALGORITHM_HMAC_SHA256}, and
109 | * {@value #TOTP_ALGORITHM_HMAC_SHA512}
110 | *
111 | * @throws UncheckedNoSuchAlgorithmException if the given algorithm is {@value #TOTP_ALGORITHM_HMAC_SHA512} and the
112 | * JVM does not support that algorithm; all JVMs are required to support {@value #TOTP_ALGORITHM_HMAC_SHA1} and
113 | * {@value #TOTP_ALGORITHM_HMAC_SHA256}, but are not required to support {@value #TOTP_ALGORITHM_HMAC_SHA512}
114 | *
115 | * @see #TOTP_ALGORITHM_HMAC_SHA1
116 | * @see #TOTP_ALGORITHM_HMAC_SHA256
117 | * @see #TOTP_ALGORITHM_HMAC_SHA512
118 | */
119 | public TimeBasedOneTimePasswordGenerator(final Duration timeStep, final int passwordLength, final String algorithm)
120 | throws UncheckedNoSuchAlgorithmException {
121 |
122 | this.hotp = new HmacOneTimePasswordGenerator(passwordLength, algorithm);
123 | this.timeStep = timeStep;
124 | }
125 |
126 | /**
127 | * Generates a one-time password using the given key and timestamp.
128 | *
129 | * @param key the key to be used to generate the password
130 | * @param timestamp the timestamp for which to generate the password
131 | *
132 | * @return an integer representation of a one-time password; callers will need to format the password for display
133 | * on their own
134 | *
135 | * @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
136 | */
137 | public int generateOneTimePassword(final Key key, final Instant timestamp) throws InvalidKeyException {
138 | return this.hotp.generateOneTimePassword(key, timestamp.toEpochMilli() / this.timeStep.toMillis());
139 | }
140 |
141 | /**
142 | * Generates a one-time password using the given key and timestamp and formats it as a string with the system
143 | * default locale.
144 | *
145 | * @param key the key to be used to generate the password
146 | * @param timestamp the timestamp for which to generate the password
147 | *
148 | * @return a string representation of a one-time password
149 | *
150 | * @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
151 | *
152 | * @see Locale#getDefault()
153 | */
154 | public String generateOneTimePasswordString(final Key key, final Instant timestamp) throws InvalidKeyException {
155 | return this.generateOneTimePasswordString(key, timestamp, Locale.getDefault());
156 | }
157 |
158 | /**
159 | * Generates a one-time password using the given key and timestamp and formats it as a string with the given locale.
160 | *
161 | * @param key the key to be used to generate the password
162 | * @param timestamp the timestamp for which to generate the password
163 | * @param locale the locale to apply during formatting
164 | *
165 | * @return a string representation of a one-time password
166 | *
167 | * @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
168 | */
169 | public String generateOneTimePasswordString(final Key key, final Instant timestamp, final Locale locale) throws InvalidKeyException {
170 | return this.hotp.formatOneTimePassword(this.generateOneTimePassword(key, timestamp), locale);
171 | }
172 |
173 | /**
174 | * Returns the time step used by this generator.
175 | *
176 | * @return the time step used by this generator
177 | */
178 | public Duration getTimeStep() {
179 | return this.timeStep;
180 | }
181 |
182 | /**
183 | * Returns the length, in decimal digits, of passwords produced by this generator.
184 | *
185 | * @return the length, in decimal digits, of passwords produced by this generator
186 | */
187 | public int getPasswordLength() {
188 | return this.hotp.getPasswordLength();
189 | }
190 |
191 | /**
192 | * Returns the name of the HMAC algorithm used by this generator.
193 | *
194 | * @return the name of the HMAC algorithm used by this generator
195 | */
196 | public String getAlgorithm() {
197 | return this.hotp.getAlgorithm();
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2016 Jon Chambers
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE. */
20 |
21 | package com.eatthepath.otp;
22 |
23 | import javax.crypto.Mac;
24 | import javax.crypto.ShortBufferException;
25 | import java.nio.ByteBuffer;
26 | import java.security.InvalidKeyException;
27 | import java.security.Key;
28 | import java.security.NoSuchAlgorithmException;
29 | import java.util.Locale;
30 |
31 | /**
32 | * Generates HMAC-based one-time passwords (HOTP) as specified in
33 | * RFC 4226. {@code HmacOneTimePasswordGenerator} instances are
34 | * thread-safe and may be shared between threads.
35 | *
36 | * @author Jon Chambers
37 | */
38 | public class HmacOneTimePasswordGenerator {
39 | private final Mac prototypeMac;
40 | private final int passwordLength;
41 |
42 | private final int modDivisor;
43 |
44 | private final String formatString;
45 |
46 | /**
47 | * The default length, in decimal digits, for one-time passwords.
48 | */
49 | public static final int DEFAULT_PASSWORD_LENGTH = 6;
50 |
51 | /**
52 | * The HMAC algorithm specified by the HOTP standard.
53 | */
54 | static final String HOTP_HMAC_ALGORITHM = "HmacSHA1";
55 |
56 | /**
57 | * Creates a new HMAC-based one-time password (HOTP) generator using a default password length
58 | * ({@value com.eatthepath.otp.HmacOneTimePasswordGenerator#DEFAULT_PASSWORD_LENGTH} digits).
59 | */
60 | public HmacOneTimePasswordGenerator() {
61 | this(DEFAULT_PASSWORD_LENGTH);
62 | }
63 |
64 | /**
65 | * Creates a new HMAC-based one-time password (HOTP) generator using the given password length.
66 | *
67 | * @param passwordLength the length, in decimal digits, of the one-time passwords to be generated; must be between
68 | * 6 and 8, inclusive
69 | */
70 | public HmacOneTimePasswordGenerator(final int passwordLength) {
71 | this(passwordLength, HOTP_HMAC_ALGORITHM);
72 | }
73 |
74 | /**
75 | *
Creates a new HMAC-based one-time password generator using the given password length and algorithm. Note that
76 | * RFC 4226 specifies that HOTP must always use HMAC-SHA1 as
77 | * an algorithm, but derived one-time password systems like TOTP may allow for other algorithms.
78 | *
79 | * @param passwordLength the length, in decimal digits, of the one-time passwords to be generated; must be between
80 | * 6 and 8, inclusive
81 | * @param algorithm the name of the {@link javax.crypto.Mac} algorithm to use when generating passwords; note that
82 | * HOTP only allows for {@value com.eatthepath.otp.HmacOneTimePasswordGenerator#HOTP_HMAC_ALGORITHM}, but derived
83 | * standards like TOTP may allow for other algorithms
84 | *
85 | * @throws UncheckedNoSuchAlgorithmException if the given algorithm is not supported by the underlying JRE
86 | */
87 | HmacOneTimePasswordGenerator(final int passwordLength, final String algorithm) throws UncheckedNoSuchAlgorithmException {
88 | try {
89 | this.prototypeMac = Mac.getInstance(algorithm);
90 | } catch (final NoSuchAlgorithmException e) {
91 | throw new UncheckedNoSuchAlgorithmException(e);
92 | }
93 |
94 | switch (passwordLength) {
95 | case 6: {
96 | this.modDivisor = 1_000_000;
97 | this.formatString = "%06d";
98 | break;
99 | }
100 |
101 | case 7: {
102 | this.modDivisor = 10_000_000;
103 | this.formatString = "%07d";
104 | break;
105 | }
106 |
107 | case 8: {
108 | this.modDivisor = 100_000_000;
109 | this.formatString = "%08d";
110 | break;
111 | }
112 |
113 | default: {
114 | throw new IllegalArgumentException("Password length must be between 6 and 8 digits.");
115 | }
116 | }
117 |
118 | this.passwordLength = passwordLength;
119 | }
120 |
121 | /**
122 | * Generates a one-time password using the given key and counter value.
123 | *
124 | * @param key the key to be used to generate the password
125 | * @param counter the counter value for which to generate the password
126 | *
127 | * @return an integer representation of a one-time password; callers will need to format the password for display
128 | * on their own
129 | *
130 | * @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
131 | */
132 | public int generateOneTimePassword(final Key key, final long counter) throws InvalidKeyException {
133 | final Mac mac = getMac();
134 | final ByteBuffer buffer = ByteBuffer.allocate(mac.getMacLength());
135 |
136 | buffer.putLong(0, counter);
137 |
138 | try {
139 | final byte[] array = buffer.array();
140 |
141 | mac.init(key);
142 | mac.update(array, 0, 8);
143 | mac.doFinal(array, 0);
144 | } catch (final ShortBufferException e) {
145 | // We allocated the buffer to (at least) match the size of the MAC length at construction time, so this
146 | // should never happen.
147 | throw new RuntimeException(e);
148 | }
149 |
150 | final int offset = buffer.get(buffer.capacity() - 1) & 0x0f;
151 | return (buffer.getInt(offset) & 0x7fffffff) % this.modDivisor;
152 | }
153 |
154 | private Mac getMac() {
155 | try {
156 | // Cloning is generally cheaper than `Mac.getInstance`, but isn't GUARANTEED to be supported.
157 | return (Mac) this.prototypeMac.clone();
158 | } catch (CloneNotSupportedException e) {
159 | try {
160 | return Mac.getInstance(this.prototypeMac.getAlgorithm());
161 | } catch (final NoSuchAlgorithmException ex) {
162 | // This should be impossible; we're getting the algorithm from a Mac that already exists, and so the
163 | // algorithm must be supported.
164 | throw new RuntimeException(ex);
165 | }
166 | }
167 | }
168 |
169 | /**
170 | * Generates a one-time password using the given key and counter value and formats it as a string using the system
171 | * default locale.
172 | *
173 | * @param key the key to be used to generate the password
174 | * @param counter the counter value for which to generate the password
175 | *
176 | * @return a string representation of a one-time password
177 | *
178 | * @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
179 | *
180 | * @see Locale#getDefault()
181 | */
182 | public String generateOneTimePasswordString(final Key key, final long counter) throws InvalidKeyException {
183 | return this.generateOneTimePasswordString(key, counter, Locale.getDefault());
184 | }
185 |
186 | /**
187 | * Generates a one-time password using the given key and counter value and formats it as a string using the given
188 | * locale.
189 | *
190 | * @param key the key to be used to generate the password
191 | * @param counter the counter value for which to generate the password
192 | * @param locale the locale to apply during formatting
193 | *
194 | * @return a string representation of a one-time password
195 | *
196 | * @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
197 | */
198 | public String generateOneTimePasswordString(final Key key, final long counter, final Locale locale) throws InvalidKeyException {
199 | return this.formatOneTimePassword(generateOneTimePassword(key, counter), locale);
200 | }
201 |
202 | /**
203 | * Formats a one-time password as a fixed-length string using the given locale.
204 | *
205 | * @param oneTimePassword the one-time password to format as a string
206 | * @param locale the locale to apply during formatting
207 | *
208 | * @return a string representation of the given one-time password
209 | */
210 | String formatOneTimePassword(final int oneTimePassword, final Locale locale) {
211 | return String.format(locale, formatString, oneTimePassword);
212 | }
213 |
214 | /**
215 | * Returns the length, in decimal digits, of passwords produced by this generator.
216 | *
217 | * @return the length, in decimal digits, of passwords produced by this generator
218 | */
219 | public int getPasswordLength() {
220 | return this.passwordLength;
221 | }
222 |
223 | /**
224 | * Returns the name of the HMAC algorithm used by this generator.
225 | *
226 | * @return the name of the HMAC algorithm used by this generator
227 | */
228 | public String getAlgorithm() {
229 | return this.prototypeMac.getAlgorithm();
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.eatthepath
6 | java-otp
7 | 0.4.1-SNAPSHOT
8 | java-otp
9 | A one-time password (HOTP and TOTP) library for Java
10 | 2016
11 | https://github.com/jchambers/java-otp
12 |
13 |
14 | scm:git:https://github.com/jchambers/java-otp.git
15 | scm:git:git@github.com:jchambers/java-otp.git
16 | https://github.com/jchambers/java-otp
17 | HEAD
18 |
19 |
20 |
21 |
22 | jon
23 | Jon Chambers
24 | jon.chambers@gmail.com
25 | https://github.com/jchambers
26 |
27 | developer
28 |
29 | -5
30 |
31 |
32 |
33 |
34 |
35 | The MIT License (MIT)
36 | http://opensource.org/licenses/MIT
37 | repo
38 |
39 |
40 |
41 |
42 | UTF-8
43 |
44 |
46 | 8
47 | 8
48 |
49 | 1.32
50 | 5.8.1
51 |
52 |
53 |
54 |
55 | org.junit.jupiter
56 | junit-jupiter-engine
57 | ${junit.version}
58 | test
59 |
60 |
61 |
62 | org.junit.jupiter
63 | junit-jupiter-params
64 | ${junit.version}
65 | test
66 |
67 |
68 |
69 | org.openjdk.jmh
70 | jmh-core
71 | ${jmh.version}
72 | test
73 |
74 |
75 |
76 | org.openjdk.jmh
77 | jmh-generator-annprocess
78 | ${jmh.version}
79 | test
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | org.apache.maven.plugins
88 | maven-surefire-plugin
89 | 3.0.0-M5
90 |
91 |
92 |
93 |
94 |
95 |
96 | org.codehaus.mojo
97 | build-helper-maven-plugin
98 | 3.0.0
99 |
100 |
101 | add-test-source
102 | generate-test-sources
103 |
104 | add-test-source
105 |
106 |
107 |
108 | src/benchmark/java
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | org.apache.maven.plugins
117 | maven-compiler-plugin
118 | 3.8.1
119 |
120 |
121 |
122 |
123 | testCompile
124 |
125 |
126 |
127 |
128 |
129 | org.openjdk.jmh
130 | jmh-generator-annprocess
131 | ${jmh.version}
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | org.apache.maven.plugins
141 | maven-assembly-plugin
142 | 3.1.1
143 |
144 |
145 |
146 | src/assembly/benchmark.xml
147 |
148 |
149 |
150 |
151 |
152 | make-assembly
153 | package
154 |
155 | single
156 |
157 |
158 | true
159 |
160 |
161 | org.openjdk.jmh.Main
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | org.apache.maven.plugins
171 | maven-jar-plugin
172 | 3.2.0
173 |
174 |
175 |
176 | com.eatthepath.otp
177 |
178 |
179 |
180 | **/.gitignore
181 |
182 |
183 |
184 |
185 |
186 | org.apache.maven.plugins
187 | maven-javadoc-plugin
188 | 2.9.1
189 |
190 | 8
191 | ${basedir}/src/main/java/overview.html
192 | public
193 |
194 |
195 | https://docs.oracle.com/javase/8/docs/api/
196 |
197 |
198 |
199 |
200 | attach-javadocs
201 |
202 | jar
203 |
204 |
205 |
206 |
207 |
208 |
209 | org.apache.maven.plugins
210 | maven-source-plugin
211 | 2.2.1
212 |
213 |
214 | attach-sources
215 |
216 | jar
217 |
218 |
219 |
220 |
221 |
222 |
223 | org.sonatype.plugins
224 | nexus-staging-maven-plugin
225 | 1.6.13
226 |
227 | true
228 |
229 |
230 | ossrh
231 | https://oss.sonatype.org/
232 | false
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 | java-8-release-target
241 |
242 | [9,)
243 |
244 |
245 | 8
246 |
247 |
248 |
249 |
250 | release-sign-artifacts
251 |
252 |
253 | performRelease
254 | true
255 |
256 |
257 |
258 |
259 |
260 | org.apache.maven.plugins
261 | maven-gpg-plugin
262 | 3.0.1
263 |
264 |
265 | sign-artifacts
266 | verify
267 |
268 | sign
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
--------------------------------------------------------------------------------
/src/test/java/com/eatthepath/otp/TimeBasedOneTimePasswordGeneratorTest.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2016 Jon Chambers
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE. */
20 |
21 | package com.eatthepath.otp;
22 |
23 | import org.junit.jupiter.api.Test;
24 | import org.junit.jupiter.params.ParameterizedTest;
25 | import org.junit.jupiter.params.provider.Arguments;
26 | import org.junit.jupiter.params.provider.MethodSource;
27 |
28 | import javax.crypto.Mac;
29 | import javax.crypto.spec.SecretKeySpec;
30 | import java.nio.charset.StandardCharsets;
31 | import java.security.Key;
32 | import java.security.NoSuchAlgorithmException;
33 | import java.time.Duration;
34 | import java.time.Instant;
35 | import java.util.Locale;
36 | import java.util.stream.Stream;
37 |
38 | import static org.junit.jupiter.api.Assertions.assertEquals;
39 | import static org.junit.jupiter.api.Assumptions.assumeTrue;
40 | import static org.junit.jupiter.params.provider.Arguments.arguments;
41 |
42 | public class TimeBasedOneTimePasswordGeneratorTest {
43 |
44 | private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
45 | private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
46 | private static final String HMAC_SHA512_ALGORITHM = "HmacSHA512";
47 |
48 | private static final byte[] HMAC_SHA1_KEY_BYTES =
49 | "12345678901234567890".getBytes(StandardCharsets.US_ASCII);
50 |
51 | private static final byte[] HMAC_SHA256_KEY_BYTES =
52 | "12345678901234567890123456789012".getBytes(StandardCharsets.US_ASCII);
53 |
54 | private static final byte[] HMAC_SHA512_KEY_BYTES =
55 | "1234567890123456789012345678901234567890123456789012345678901234".getBytes(StandardCharsets.US_ASCII);
56 |
57 | @Test
58 | void testGetPasswordLength() {
59 | final int passwordLength = 7;
60 | assertEquals(passwordLength, new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), passwordLength).getPasswordLength());
61 | }
62 |
63 | @Test
64 | void testGetAlgorithm() {
65 | final String algorithm = TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256;
66 | assertEquals(algorithm, new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 6, algorithm).getAlgorithm());
67 | }
68 |
69 | @Test
70 | void testGetTimeStep() {
71 | final Duration timeStep = Duration.ofSeconds(97);
72 | assertEquals(timeStep, new TimeBasedOneTimePasswordGenerator(timeStep).getTimeStep());
73 | }
74 |
75 | /**
76 | * Tests time-based one-time password generation using the test vectors from
77 | * RFC 6238, Appendix B. Note that the RFC
78 | * incorrectly states that the same key is used for all test vectors. The
79 | * errata correctly points out that
80 | * different keys are used for each of the various HMAC algorithms.
81 | */
82 | @ParameterizedTest
83 | @MethodSource("argumentsForTestGenerateOneTimePasswordTotp")
84 | void testGenerateOneTimePasswordTotp(final String algorithm, final byte[] keyBytes, final long epochSeconds, final int expectedOneTimePassword) throws Exception {
85 | assumeAlgorithmSupported(algorithm);
86 |
87 | final TimeBasedOneTimePasswordGenerator totp =
88 | new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 8, algorithm);
89 |
90 | final Instant timestamp = Instant.ofEpochSecond(epochSeconds);
91 | final Key key = new SecretKeySpec(keyBytes, algorithm);
92 |
93 | assertEquals(expectedOneTimePassword, totp.generateOneTimePassword(key, timestamp));
94 | }
95 |
96 | private static Stream argumentsForTestGenerateOneTimePasswordTotp() {
97 | return Stream.of(
98 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 59L, 94287082),
99 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111109L, 7081804),
100 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111111L, 14050471),
101 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1234567890L, 89005924),
102 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 2000000000L, 69279037),
103 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 20000000000L, 65353130),
104 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 59L, 46119246),
105 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1111111109L, 68084774),
106 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1111111111L, 67062674),
107 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1234567890L, 91819424),
108 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 2000000000L, 90698825),
109 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 20000000000L, 77737706),
110 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 59L, 90693936),
111 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1111111109L, 25091201),
112 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1111111111L, 99943326),
113 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1234567890L, 93441116),
114 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 2000000000L, 38618901),
115 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 20000000000L, 47863826)
116 | );
117 | }
118 |
119 | @ParameterizedTest
120 | @MethodSource("argumentsForTestGenerateOneTimePasswordStringTotp")
121 | void testGenerateOneTimePasswordStringTotp(final String algorithm, final byte[] keyBytes, final long epochSeconds, final String expectedOneTimePassword) throws Exception {
122 | assumeAlgorithmSupported(algorithm);
123 |
124 | final TimeBasedOneTimePasswordGenerator totp =
125 | new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 8, algorithm);
126 |
127 | final Instant timestamp = Instant.ofEpochSecond(epochSeconds);
128 | final Key key = new SecretKeySpec(keyBytes, algorithm);
129 |
130 | assertEquals(expectedOneTimePassword, totp.generateOneTimePasswordString(key, timestamp));
131 | }
132 |
133 | private static Stream argumentsForTestGenerateOneTimePasswordStringTotp() {
134 | return Stream.of(
135 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 59L, "94287082"),
136 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111109L, "07081804"),
137 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111111L, "14050471"),
138 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1234567890L, "89005924"),
139 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 2000000000L, "69279037"),
140 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 20000000000L, "65353130"),
141 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 59L, "46119246"),
142 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1111111109L, "68084774"),
143 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1111111111L, "67062674"),
144 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1234567890L, "91819424"),
145 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 2000000000L, "90698825"),
146 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 20000000000L, "77737706"),
147 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 59L, "90693936"),
148 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1111111109L, "25091201"),
149 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1111111111L, "99943326"),
150 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1234567890L, "93441116"),
151 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 2000000000L, "38618901"),
152 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 20000000000L, "47863826")
153 | );
154 | }
155 |
156 | @ParameterizedTest
157 | @MethodSource("argumentsForTestGenerateOneTimePasswordStringLocaleTotp")
158 | void testGenerateOneTimePasswordStringLocaleTotp(final String algorithm, final byte[] keyBytes, final long epochSeconds, final Locale locale, final String expectedOneTimePassword) throws Exception {
159 | assumeAlgorithmSupported(algorithm);
160 |
161 | final TimeBasedOneTimePasswordGenerator totp =
162 | new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 8, algorithm);
163 |
164 | final Instant timestamp = Instant.ofEpochSecond(epochSeconds);
165 | final Key key = new SecretKeySpec(keyBytes, algorithm);
166 |
167 | assertEquals(expectedOneTimePassword, totp.generateOneTimePasswordString(key, timestamp, locale));
168 | }
169 |
170 | private static Stream argumentsForTestGenerateOneTimePasswordStringLocaleTotp() {
171 | final Locale locale = Locale.forLanguageTag("hi-IN-u-nu-Deva");
172 |
173 | return Stream.of(
174 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 59L, locale, "९४२८७०८२"),
175 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111109L, locale, "०७०८१८०४"),
176 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1111111111L, locale, "१४०५०४७१"),
177 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 1234567890L, locale, "८९००५९२४"),
178 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 2000000000L, locale, "६९२७९०३७"),
179 | arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY_BYTES, 20000000000L, locale, "६५३५३१३०"),
180 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 59L, locale, "४६११९२४६"),
181 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1111111109L, locale, "६८०८४७७४"),
182 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1111111111L, locale, "६७०६२६७४"),
183 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 1234567890L, locale, "९१८१९४२४"),
184 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 2000000000L, locale, "९०६९८८२५"),
185 | arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY_BYTES, 20000000000L, locale, "७७७३७७०६"),
186 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 59L, locale, "९०६९३९३६"),
187 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1111111109L, locale, "२५०९१२०१"),
188 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1111111111L, locale, "९९९४३३२६"),
189 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 1234567890L, locale, "९३४४१११६"),
190 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 2000000000L, locale, "३८६१८९०१"),
191 | arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY_BYTES, 20000000000L, locale, "४७८६३८२६")
192 | );
193 | }
194 |
195 | private static void assumeAlgorithmSupported(final String algorithm) {
196 | boolean algorithmSupported = true;
197 |
198 | try {
199 | Mac.getInstance(algorithm);
200 | } catch (final NoSuchAlgorithmException e) {
201 | algorithmSupported = false;
202 | }
203 |
204 | assumeTrue(algorithmSupported, "Algorithm not supported: " + algorithm);
205 | }
206 | }
207 |
--------------------------------------------------------------------------------