├── .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 | 25 | java-otp overview 26 | 27 | 28 | 29 |

java-otp is a Java library for generating HOTP (RFC 4226) or TOTP (RFC 6238) one-time passwords.

30 | 31 |

Usage

32 | 33 |

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:

38 | 39 |
final Key secretKey;
40 | {
41 |     final KeyGenerator keyGenerator = KeyGenerator.getInstance(totp.getAlgorithm());
42 |     keyGenerator.init(160);
43 | 
44 |     secretKey = keyGenerator.generateKey();
45 | }
46 | 47 |

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 | [![Build status](https://travis-ci.org/jchambers/java-otp.svg?branch=master)](https://travis-ci.org/jchambers/java-otp) 2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.eatthepath/java-otp/badge.svg)](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 | --------------------------------------------------------------------------------