Repository: BastiaanJansen/otp-java
Branch: main
Commit: b6fd6e49f6f8
Files: 16
Total size: 67.3 KB
Directory structure:
gitextract_cwl01tct/
├── .github/
│ └── workflows/
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src/
├── main/
│ └── java/
│ └── com/
│ └── bastiaanjansen/
│ └── otp/
│ ├── HMACAlgorithm.java
│ ├── HOTPGenerator.java
│ ├── SecretGenerator.java
│ ├── TOTPGenerator.java
│ └── helpers/
│ └── URIHelper.java
└── test/
└── java/
└── com/
└── bastiaanjansen/
└── otp/
├── ExampleApp.java
├── HOTPGeneratorTest.java
├── SecretGeneratorTest.java
├── TOTPGeneratorTest.java
└── helpers/
└── URIHelperTest.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yml
================================================
name: Build & Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
name: Java ${{ matrix.java }} build
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-package: jdk
java-version: ${{ matrix.java }}
- name: Build with Maven
run: mvn -B package --file pom.xml
- run: mkdir artifacts && cp target/*.jar artifacts
- name: Upload Maven build artifact
uses: actions/upload-artifact@v3
with:
name: build-java-${{ matrix.java }}.jar
path: artifacts
test:
name: Java ${{ matrix.java }} test
needs: [build]
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Run tests with Maven
run: mvn -B test --file pom.xml
================================================
FILE: .github/workflows/publish.yml
================================================
name: Maven Package
on:
release:
types: [ created ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]
name: Java ${{ matrix.java }} build
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Build with Maven
run: mvn -B package --file pom.xml
- run: mkdir artifacts && cp target/*.jar artifacts
- name: Upload Maven build artifact
uses: actions/upload-artifact@v3
with:
name: build-java-${{ matrix.java }}.jar
path: artifacts
test:
needs: [ build ]
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]
name: Java ${{ matrix.java }} test
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Run tests with Maven
run: mvn -B test --file pom.xml
publish-maven-central:
name: Publish to Maven Central Repository
needs: [ build, test ]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'
- name: Publish package
uses: samuelmeuli/action-maven-publish@v1
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }}
nexus_username: ${{ secrets.NEXUS_USERNAME }}
nexus_password: ${{ secrets.NEXUS_PASSWORD }}
maven_profiles: "deploy-maven-central"
publish-github-registry:
name: Publish to GitHub Registry
needs: [ build, test ]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
server-id: github
java-version: '11'
distribution: 'adopt'
- name: Publish package
run: mvn --batch-mode --activate-profiles "deploy-github-registry" deploy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
.idea
target
.m2
pom.xml.releaseBackup
release.properties
.DS_Store
*.iml
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Bastiaan Jansen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# OTP-Java
[](https://github.com/BastiaanJansen/otp-java/actions/workflows/build.yml)
[](https://www.codacy.com/gh/BastiaanJansen/OTP-Java/dashboard?utm_source=github.com&utm_medium=referral&utm_content=BastiaanJansen/OTP-Java&utm_campaign=Badge_Grade)


A small and easy-to-use one-time password generator for Java implementing [RFC 4226](https://tools.ietf.org/html/rfc4226) (HOTP) and [RFC 6238](https://tools.ietf.org/html/rfc6238) (TOTP).
## Table of Contents
* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [HOTP (Counter-based one-time passwords)](#counter-based-one-time-passwords)
* [TOTP (Time-based one-time passwords)](#time-based-one-time-passwords)
* [Recovery codes](#recovery-codes)
## Features
The following features are supported:
1. Generation of secrets
2. Time-based one-time password (TOTP, RFC 6238) generation based on current time, specific time, OTPAuth URI and more for different HMAC algorithms.
3. HMAC-based one-time password (HOTP, RFC 4226) generation based on counter and OTPAuth URI.
4. Verification of one-time passwords
5. Generation of OTP Auth URI's
## Installation
### Maven
```xml
<dependency>
<groupId>com.github.bastiaanjansen</groupId>
<artifactId>otp-java</artifactId>
<version>2.1.0</version>
</dependency>
```
### Gradle
```gradle
implementation 'com.github.bastiaanjansen:otp-java:2.1.0'
```
### Scala SBT
```scala
libraryDependencies += "com.github.bastiaanjansen" % "otp-java" % "2.1.0"
```
### Apache Ivy
```xml
<dependency org="com.github.bastiaanjansen" name="otp-java" rev="2.1.0" />
```
Or you can download the source from the [GitHub releases page](https://github.com/BastiaanJansen/OTP-Java/releases).
## Usage
### HOTP (Counter-based one-time passwords)
#### Initialization HOTP instance
To create a `HOTPGenerator` instance, use the `HOTPGenerator.Builder` class as follows:
```java
String secret = "VV3KOX7UQJ4KYAKOHMZPPH3US4CJIMH6F3ZKNB5C2OOBQ6V2KIYHM27Q";
HOTPGenerator hotp = new HOTPGenerator.Builder(secret).build();
```
The above builder creates a HOTP instance with default values for passwordLength = 6 and algorithm = SHA1. Use the builder to change these defaults:
```java
HOTPGenerator hotp = new HOTPGenerator.Builder(secret)
.withPasswordLength(8)
.withAlgorithm(HMACAlgorithm.SHA256)
.build();
```
If you have a shared secret described in [RFC-4226](https://www.rfc-editor.org/rfc/rfc4226), you need to encode it first:
```java
byte[] sharedSecret = getMySharedSecret();
byte[] secret = Base32.encode(sharedSecret);
```
When you don't already have a secret, you can let the library generate it:
```java
// To generate a Base32-encoded secret with 160 bits
byte[] secret = SecretGenerator.generate();
// To generate a Base32-encoded secret with a custom amount of bits
byte[] secret = SecretGenerator.generate(512);
```
It is also possible to create a HOTP instance based on an OTPAuth URI. When algorithm or digits are not specified, the default values will be used.
```java
URI uri = new URI("otpauth://hotp/issuer?secret=ABCDEFGHIJKLMNOP&algorithm=SHA1&digits=6&counter=8237");
HOTPGenerator hotp = HOTPGenerator.fromURI(uri);
```
Get information about the generator:
```java
int passwordLength = hotp.getPasswordLength(); // 6
HMACAlgorithm algorithm = hotp.getAlgorithm(); // HMACAlgorithm.SHA1
```
#### Generation of HOTP code
After creating an instance of the HOTP class, a code can be generated by using the `generate()` method:
```java
try {
int counter = 5;
String code = hotp.generate(counter);
// To verify a token:
boolean isValid = hotp.verify(code, counter);
// Or verify with a delay window
boolean isValid = hotp.verify(code, counter, 2);
} catch (IllegalStateException e) {
// Handle error
}
```
### TOTP (Time-based one-time passwords)
#### Initialization TOTP instance
TOTP can accept more paramaters: `passwordLength`, `period`, `algorithm` and `secret`. The default values are: passwordLength = 6, period = 30 and algorithm = SHA1.
```java
// Generate a secret (or use your own secret)
byte[] secret = SecretGenerator.generate();
TOTPGenerator totp = new TOTPGenerator.Builder(secret)
.withHOTPGenerator(builder -> {
builder.withPasswordLength(6);
builder.withAlgorithm(HMACAlgorithm.SHA1); // SHA256 and SHA512 are also supported
})
.withPeriod(Duration.ofSeconds(30))
.build();
```
Or create a `TOTP` instance from an OTPAuth URI:
```java
URI uri = new URI("otpauth://totp/issuer?secret=ABCDEFGHIJKLMNOP&algorithm=SHA1&digits=6&period=30");
TOTPGenerator totpGenerator = TOTPGenerator.fromURI(uri);
```
Get information about the generator:
```java
int passwordLength = totpGenerator.getPasswordLength(); // 6
HMACAlgorithm algorithm = totpGenerator.getAlgorithm(); // HMACAlgorithm.SHA1
Duration period = totpGenerator.getPeriod(); // Duration.ofSeconds(30)
```
#### Generation of TOTP code
After creating an instance of the TOTP class, a code can be generated by using the `now()` method, similarly with the HOTP class:
```java
try {
String code = totpGenerator.now();
// To verify a token:
boolean isValid = totpGenerator.verify(code);
} catch (IllegalStateException e) {
// Handle error
}
```
The above code will generate a time-based one-time password based on the current time. The API supports, besides the current time, the creation of codes based on `timeSince1970` in seconds, `Date`, and `Instant`:
```java
try {
// Based on current time
totpGenerator.now();
// Based on specific date
totpGenerator.at(new Date());
// Based on specific local date
totpGenerator.at(LocalDate.of(2023, 3, 2));
// Based on seconds past 1970
totpGenerator.at(9238346823);
// Based on an instant
totpGenerator.at(Instant.now());
} catch (IllegalStateException e) {
// Handle error
}
```
### Generation of OTPAuth URI's
To easily generate a OTPAuth URI for easy on-boarding, use the `getURI()` method for both `HOTP` and `TOTP`. Example for `TOTP`:
```java
TOTPGenerator totpGenerator = new TOTPGenerator.Builder(secret).build();
URI uri = totpGenerator.getURI("issuer", "account"); // otpauth://totp/issuer:account?period=30&digits=6&secret=SECRET&algorithm=SHA1
```
## Recovery Codes
Often, services provide "backup codes" or "recovery codes" which can be used when the user cannot access the 2FA device anymore. Often because 2FA device is a mobile phone, which can be lost or stolen.
Because recovery code generation is not part of the specifications of OTP, it is not possible to generate recovery codes with this library and should be implemented seperately.
## Licence
OTP-Java is available under the MIT License. See the LICENCE for more info.
[](https://github.com/BastiaanJansen/otp-java/stargazers)
================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.sonatype.oss</groupId>
<artifactId>oss-parent</artifactId>
<version>9</version>
</parent>
<groupId>com.github.bastiaanjansen</groupId>
<artifactId>otp-java</artifactId>
<version>2.1.0</version>
<name>OTP-Java</name>
<description>A small and easy-to-use one-time password generator for Java according to RFC 4226 (HOTP) and RFC 6238 (TOTP).</description>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<licenses>
<license>
<name>MIT License</name>
<url>https://github.com/BastiaanJansen/otp-java/blob/main/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<scm>
<connection>scm:git:https://github.com/BastiaanJansen/otp-java.git</connection>
<url>http://github.com/BastiaanJansen/otp-java</url>
<developerConnection>scm:git:https://github.com/BastiaanJansen/otp-java.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>java-hamcrest</artifactId>
<version>2.0.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<!-- Deployment profile (required so these plugins are only used when deploying) -->
<profile>
<id>deploy-maven-central</id>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.8</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<!-- Source plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Javadoc plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- GPG plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<!-- Prevent `gpg` from using pinentry programs -->
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>deploy-github-registry</id>
<distributionManagement>
<repository>
<id>github</id>
<name>GitHub Packages</name>
<url>https://maven.pkg.github.com/BastiaanJansen/otp-java</url>
</repository>
</distributionManagement>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>
================================================
FILE: src/main/java/com/bastiaanjansen/otp/HMACAlgorithm.java
================================================
package com.bastiaanjansen.otp;
/**
* HMAC algorithm enum
* @author Bastiaan Jansen
*/
public enum HMACAlgorithm {
@Deprecated
SHA1("HmacSHA1"),
SHA224("HmacSHA224"),
SHA256("HmacSHA256"),
SHA384("HmacSHA384"),
SHA512("HmacSHA512");
private final String name;
HMACAlgorithm(String name) {
this.name = name;
}
public String getHMACName() {
return name;
}
}
================================================
FILE: src/main/java/com/bastiaanjansen/otp/HOTPGenerator.java
================================================
package com.bastiaanjansen.otp;
import com.bastiaanjansen.otp.helpers.URIHelper;
import org.apache.commons.codec.binary.Base32;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
public final class HOTPGenerator {
private static final String URL_SCHEME = "otpauth";
private static final int DEFAULT_PASSWORD_LENGTH = 6;
private static final HMACAlgorithm DEFAULT_HMAC_ALGORITHM = HMACAlgorithm.SHA1;
private static final String OTP_TYPE = "hotp";
private final int passwordLength;
private final HMACAlgorithm algorithm;
private final byte[] secret;
private HOTPGenerator(final Builder builder) {
this.passwordLength = builder.passwordLength;
this.algorithm = builder.algorithm;
this.secret = builder.secret;
}
public static HOTPGenerator fromURI(final URI uri) throws URISyntaxException {
Map<String, String> query = URIHelper.queryItems(uri);
byte[] secret = Optional.ofNullable(query.get(URIHelper.SECRET))
.map(s -> s.getBytes(UTF_8))
.orElseThrow(() -> new IllegalArgumentException("Secret query parameter must be set"));
Builder builder = new Builder(secret);
try {
Optional.ofNullable(query.get(URIHelper.DIGITS))
.map(Integer::valueOf)
.ifPresent(builder::withPasswordLength);
Optional.ofNullable(query.get(URIHelper.ALGORITHM))
.map(String::toUpperCase)
.map(HMACAlgorithm::valueOf)
.ifPresent(builder::withAlgorithm);
} catch (Exception e) {
throw new URISyntaxException(uri.toString(), "URI could not be parsed");
}
return builder.build();
}
public static HOTPGenerator withDefaultValues(final byte[] secret) {
return new HOTPGenerator.Builder(secret).build();
}
public URI getURI(final int counter, final String issuer) throws URISyntaxException {
return getURI(counter, issuer, "");
}
public URI getURI(final int counter, final String issuer, final String account) throws URISyntaxException {
Map<String, String> query = new HashMap<>();
query.put(URIHelper.COUNTER, String.valueOf(counter));
return getURI(OTP_TYPE, issuer, account, query);
}
public int getPasswordLength() {
return passwordLength;
}
public HMACAlgorithm getAlgorithm() {
return algorithm;
}
public boolean verify(final String code, final long counter) {
return verify(code, counter, 0);
}
public boolean verify(final String code, final long counter, final int delayWindow) {
if (code.length() != passwordLength) return false;
for (int i = -delayWindow; i <= delayWindow; i++) {
String currentCode = generate(counter + i);
if (code.equals(currentCode)) return true;
}
return false;
}
public String generate(final long counter) throws IllegalStateException {
if (counter < 0)
throw new IllegalArgumentException("Counter must be greater than or equal to 0");
byte[] secretBytes = decodeBase32(secret);
byte[] counterBytes = longToBytes(counter);
byte[] hash;
try {
hash = generateHash(secretBytes, counterBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException();
}
return getCodeFromHash(hash);
}
public URI getURI(final String type, final String issuer, final String account, final Map<String, String> query) throws URISyntaxException {
query.put(URIHelper.DIGITS, String.valueOf(passwordLength));
query.put(URIHelper.ALGORITHM, algorithm.name());
query.put(URIHelper.SECRET, new String(secret, UTF_8));
query.put(URIHelper.ISSUER, issuer);
String path = account.isEmpty() ? URIHelper.encode(issuer) : String.format("%s:%s", URIHelper.encode(issuer), URIHelper.encode(account));
return URIHelper.createURI(URL_SCHEME, type, path, query);
}
/**
* Decode a base32 value to bytes array
*
* @param value base32 value
* @return bytes array
*/
private byte[] decodeBase32(final byte[] value) {
Base32 codec = new Base32();
return codec.decode(value);
}
private byte[] longToBytes(final long value) {
return ByteBuffer.allocate(Long.BYTES).putLong(value).array();
}
private byte[] generateHash(final byte[] secret, final byte[] data) throws InvalidKeyException, NoSuchAlgorithmException {
// Create a secret key with correct SHA algorithm
SecretKeySpec signKey = new SecretKeySpec(secret, "RAW");
// Mac is 'message authentication code' algorithm (RFC 2104)
Mac mac = Mac.getInstance(algorithm.getHMACName());
mac.init(signKey);
// Hash data with generated sign key
return mac.doFinal(data);
}
private String getCodeFromHash(final byte[] hash) {
/* Find mask to get last 4 digits:
1. Set all bits to 1: ~0 -> 11111111 -> 255 decimal -> 0xFF
2. Shift n (in this case 4, because we want the last 4 bits) bits to left with <<
3. Negate the result: 1111 1100 -> 0000 0011
*/
int mask = ~(~0 << 4);
/* Get last 4 bits of hash as offset:
Use the bitwise AND (&) operator to select last 4 bits
Mask should be 00001111 = 15 = 0xF
Last byte of hash & 0xF = last 4 bits:
Example:
Input: decimal 219 as binary: 11011011 &
Mask: decimal 15 as binary: 00001111
-----------------------------------------
Output: decimal 11 as binary: 00001011
*/
byte lastByte = hash[hash.length - 1];
int offset = lastByte & mask;
// Get 4 bytes from hash from offset to offset + 3
byte[] truncatedHashInBytes = { hash[offset], hash[offset + 1], hash[offset + 2], hash[offset + 3] };
// Wrap in ByteBuffer to convert bytes to long
ByteBuffer byteBuffer = ByteBuffer.wrap(truncatedHashInBytes);
long truncatedHash = byteBuffer.getInt();
// Mask most significant bit
truncatedHash &= 0x7FFFFFFF;
// Modulo (%) truncatedHash by 10^passwordLength
truncatedHash %= Math.pow(10, passwordLength);
// Left pad with 0s for an n-digit code
return String.format("%0" + passwordLength + "d", truncatedHash);
}
public static final class Builder {
private int passwordLength;
private HMACAlgorithm algorithm;
/**
* Base32 encoded secret
*/
private final byte[] secret;
/**
* Creates a new builder.
* <p>
* Use {@link SecretGenerator#generate()} to create a secret.
* <p>
* If you are using a shared secret from another generator, you would likely need to encode it using
* {@link org.apache.commons.codec.binary.Base32#encode(byte[])}}
*
* @param secret Base32 encoded secret
*/
public Builder(final byte[] secret) {
if (secret.length == 0)
throw new IllegalArgumentException("Secret must not be empty");
this.secret = secret;
this.passwordLength = DEFAULT_PASSWORD_LENGTH;
this.algorithm = DEFAULT_HMAC_ALGORITHM;
}
/**
* @param secret Base32 encoded secret
*/
public Builder(String secret) {
this(secret.getBytes(UTF_8));
}
public Builder withPasswordLength(final int passwordLength) {
if (!passwordLengthIsValid(passwordLength))
throw new IllegalArgumentException("Password length must be between 6 and 8 digits");
this.passwordLength = passwordLength;
return this;
}
public Builder withAlgorithm(final HMACAlgorithm algorithm) {
this.algorithm = algorithm;
return this;
}
public HOTPGenerator build() {
return new HOTPGenerator(this);
}
private boolean passwordLengthIsValid(final int passwordLength) {
return passwordLength >= 6 && passwordLength <= 8;
}
}
}
================================================
FILE: src/main/java/com/bastiaanjansen/otp/SecretGenerator.java
================================================
package com.bastiaanjansen.otp;
import org.apache.commons.codec.binary.Base32;
import java.security.SecureRandom;
/**
* A secret generator to generate OTP secrets
*
* @author Bastiaan Jansen
*/
public class SecretGenerator {
private SecretGenerator() {}
/**
* Default amount of bits for secret generation
*/
public static final int DEFAULT_BITS = 160;
private static final SecureRandom random = new SecureRandom();
private static final Base32 encoder = new Base32();
/**
* Generate an OTP base32 secret with default amount of bits
*
* @return generated secret
*/
public static byte[] generate() {
return generate(DEFAULT_BITS);
}
/**
* Generate an OTP base32 secret
*
* @param bits length, this should be greater than or equal to the length of the HMAC algorithm type:
* SHA1: 160 bits
* SHA256: 256 bits
* SHA512: 512 bits
* @return generated secret
*/
public static byte[] generate(final int bits) {
if (bits <= 0)
throw new IllegalArgumentException("Bits must be greater than or equal to 0");
byte[] bytes = new byte[bits / Byte.SIZE];
random.nextBytes(bytes);
return encoder.encode(bytes);
}
}
================================================
FILE: src/main/java/com/bastiaanjansen/otp/TOTPGenerator.java
================================================
package com.bastiaanjansen.otp;
import com.bastiaanjansen.otp.helpers.URIHelper;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static java.nio.charset.StandardCharsets.UTF_8;
public final class TOTPGenerator {
private static final String OTP_TYPE = "totp";
private static final Duration DEFAULT_PERIOD = Duration.ofSeconds(30);
private static final Clock DEFAULT_CLOCK = Clock.system(ZoneId.systemDefault());
private final Duration period;
private final Clock clock;
private final HOTPGenerator hotpGenerator;
private TOTPGenerator(final Builder builder) {
this.period = builder.period;
this.clock = builder.clock;
this.hotpGenerator = builder.hotpBuilder.build();
}
public static TOTPGenerator fromURI(URI uri) throws URISyntaxException {
Map<String, String> query = URIHelper.queryItems(uri);
String secret = Optional.ofNullable(query.get(URIHelper.SECRET))
.orElseThrow(() -> new IllegalArgumentException("Secret query parameter must be set"));
Builder builder = new Builder(secret);
try {
Optional.ofNullable(query.get(URIHelper.PERIOD))
.map(Long::parseLong)
.map(Duration::ofSeconds)
.ifPresent(builder::withPeriod);
Optional.ofNullable(query.get(URIHelper.DIGITS))
.map(Integer::valueOf)
.ifPresent(builder.hotpBuilder::withPasswordLength);
Optional.ofNullable(query.get(URIHelper.ALGORITHM))
.map(String::toUpperCase)
.map(HMACAlgorithm::valueOf)
.ifPresent(builder.hotpBuilder::withAlgorithm);
} catch (Exception e) {
throw new URISyntaxException(uri.toString(), "URI could not be parsed");
}
return builder.build();
}
public static TOTPGenerator withDefaultValues(final byte[] secret) {
return new TOTPGenerator.Builder(secret).build();
}
public String now() throws IllegalStateException {
long counter = calculateCounter(clock, period);
return hotpGenerator.generate(counter);
}
public String now(Clock clock) throws IllegalStateException {
long counter = calculateCounter(clock, period);
return hotpGenerator.generate(counter);
}
public String at(final Instant instant) throws IllegalStateException {
return at(instant.getEpochSecond());
}
public String at(final Date date) throws IllegalStateException {
long secondsSince1970 = TimeUnit.MILLISECONDS.toSeconds(date.getTime());
return at(secondsSince1970);
}
public String at(final LocalDate date) throws IllegalStateException {
long secondsSince1970 = date.atStartOfDay(clock.getZone()).toEpochSecond();
return at(secondsSince1970);
}
public String at(final long secondsPast1970) throws IllegalArgumentException {
if (!validateTime(secondsPast1970))
throw new IllegalArgumentException("Time must be above zero");
long counter = calculateCounter(secondsPast1970, period);
return hotpGenerator.generate(counter);
}
public boolean verify(final String code) {
long counter = calculateCounter(clock, period);
return hotpGenerator.verify(code, counter);
}
/**
* Checks whether a code is valid for a specific counter taking a delay window into account
*
* @param code an OTP code
* @param delayWindow window in which a code can still be deemed valid
* @return a boolean, true if code is valid, otherwise false
*/
public boolean verify(final String code, final int delayWindow) {
long counter = calculateCounter(clock, period);
return hotpGenerator.verify(code, counter, delayWindow);
}
public URI getURI(final String issuer) throws URISyntaxException {
return getURI(issuer, "");
}
public URI getURI(final String issuer, final String account) throws URISyntaxException {
Map<String, String> query = new HashMap<>();
query.put(URIHelper.PERIOD, String.valueOf(period.getSeconds()));
return hotpGenerator.getURI(OTP_TYPE, issuer, account, query);
}
/**
* Calculates time until next time window will be reached and a new totp should be generated
*
* @return a duration object with duration until next time window
*/
public Duration durationUntilNextTimeWindow() {
return durationUntilNextTimeWindow(clock);
}
public Duration durationUntilNextTimeWindow(Clock clock) {
long timeInterval = period.toMillis();
return Duration.ofMillis(timeInterval - clock.millis() % timeInterval);
}
public Duration getPeriod() {
return period;
}
public Clock getClock() {
return clock;
}
public HMACAlgorithm getAlgorithm() {
return hotpGenerator.getAlgorithm();
}
public int getPasswordLength() {
return hotpGenerator.getPasswordLength();
}
private long calculateCounter(final long secondsPast1970, final Duration period) {
return TimeUnit.SECONDS.toMillis(secondsPast1970) / period.toMillis();
}
private long calculateCounter(final Clock clock, final Duration period) {
return clock.millis() / period.toMillis();
}
private boolean validateTime(final long time) {
return time > 0;
}
public static final class Builder {
private Duration period;
private Clock clock;
private final HOTPGenerator.Builder hotpBuilder;
/**
* Creates a new builder.
* <p>
* Use {@link SecretGenerator#generate()} to create a secret.
* <p>
* If you are using a shared secret from another generator, you would likely need to encode it using
* {@link org.apache.commons.codec.binary.Base32#encode(byte[])}}
*
* @param secret Base32 encoded secret
*/
public Builder(byte[] secret) {
this.period = DEFAULT_PERIOD;
this.clock = DEFAULT_CLOCK;
this.hotpBuilder = new HOTPGenerator.Builder(secret);
}
/**
* @param secret Base32 encoded secret
*/
public Builder(String secret) {
this(secret.getBytes(UTF_8));
}
public Builder withHOTPGenerator(Consumer<HOTPGenerator.Builder> builder) {
builder.accept(hotpBuilder);
return this;
}
public Builder withClock(Clock clock) {
this.clock = clock;
return this;
}
public Builder withPeriod(Duration period) {
if (period.getSeconds() < 1) throw new IllegalArgumentException("Period must be at least 1 second");
this.period = period;
return this;
}
public TOTPGenerator build() {
return new TOTPGenerator(this);
}
}
}
================================================
FILE: src/main/java/com/bastiaanjansen/otp/helpers/URIHelper.java
================================================
package com.bastiaanjansen.otp.helpers;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A URI utility class with helper methods
*
* @author Bastiaan Jansen
*/
public class URIHelper {
public static final String DIGITS = "digits";
public static final String SECRET = "secret";
public static final String ALGORITHM = "algorithm";
public static final String PERIOD = "period";
public static final String COUNTER = "counter";
public static final String ISSUER = "issuer";
private URIHelper() {}
/**
* Get a map of query items from URI
*
* @param uri to get query items from
* @return map of query items from URI
*/
public static Map<String, String> queryItems(URI uri) {
Map<String, String> items = new LinkedHashMap<>();
String query = uri.getQuery();
String[] pairs = query.split("&");
for (String pair: pairs) {
int index = pair.indexOf("=");
try {
items.put(
URLDecoder.decode(pair.substring(0, index), StandardCharsets.UTF_8.toString()),
URLDecoder.decode(pair.substring(index + 1), StandardCharsets.UTF_8.toString())
);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Encoding should be supported");
}
}
return items;
}
/**
* Create a URI based on a scheme, host, path and query items
*
* @param scheme of URI
* @param host of URI
* @param path of URI
* @param query of URI
* @return created URI
* @throws URISyntaxException when URI cannot be created
*/
public static URI createURI(String scheme, String host, String path, Map<String, String> query) throws URISyntaxException {
String uriString = String.format("%s://%s/%s?", scheme, host, path);
String uri = query.keySet().stream()
.map(key -> String.format("%s=%s", key, encode(query.get(key))))
.collect(Collectors.joining("&", uriString, ""));
return new URI(uri);
}
public static String encode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
}
================================================
FILE: src/test/java/com/bastiaanjansen/otp/ExampleApp.java
================================================
package com.bastiaanjansen.otp;
import java.time.Clock;
import java.time.Duration;
public class ExampleApp {
public static void main(String[] args) {
// Generate a secret, if you don't have one already
byte[] secret = SecretGenerator.generate();
Clock clock = Clock.systemUTC();
// Create a TOTPGenerate instance
TOTPGenerator totpGenerator = new TOTPGenerator.Builder(secret)
.withHOTPGenerator(builder -> {
builder.withAlgorithm(HMACAlgorithm.SHA1);
builder.withPasswordLength(6);
})
.withClock(clock)
.withPeriod(Duration.ofMinutes(15))
.build();
try {
String code = totpGenerator.now();
System.out.println("Generated code: " + code);
// To verify a code
totpGenerator.verify(code); // true
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
================================================
FILE: src/test/java/com/bastiaanjansen/otp/HOTPGeneratorTest.java
================================================
package com.bastiaanjansen.otp;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.stream.Stream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
class HOTPGeneratorTest {
private final String secret = "vv3kox7uqj4kyakohmzpph3us4cjimh6f3zknb5c2oobq6v2kiyhm27q";
private static Stream<Arguments> testData() {
return Stream.of(
Arguments.of(6, 1, HMACAlgorithm.SHA1, "560287"),
Arguments.of(7, 1, HMACAlgorithm.SHA1, "1560287"),
Arguments.of(8, 1, HMACAlgorithm.SHA1, "61560287"),
Arguments.of(6, 1000, HMACAlgorithm.SHA1, "401796"),
Arguments.of(6, 923892, HMACAlgorithm.SHA1, "793394"),
Arguments.of(6, 82764924, HMACAlgorithm.SHA1, "022826"),
Arguments.of(6, 1, HMACAlgorithm.SHA256, "361406"),
Arguments.of(6, 1, HMACAlgorithm.SHA512, "016738"),
Arguments.of(6, 1, HMACAlgorithm.SHA224, "422784"),
Arguments.of(6, 1, HMACAlgorithm.SHA384, "466320")
);
}
@ParameterizedTest
@MethodSource("testData")
void generateWithCounter(int passwordLength, long counter, HMACAlgorithm algorithm, String otp) {
HOTPGenerator generator = new HOTPGenerator.Builder(secret)
.withPasswordLength(passwordLength)
.withAlgorithm(algorithm)
.build();
assertThat(generator.generate(counter), is(otp));
}
@ParameterizedTest
@ValueSource(ints = {-1, -100})
void generateWithInvalidCounter_throwsIllegalArgumentException(long counter) {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
assertThrows(IllegalArgumentException.class, () -> generator.generate(counter));
}
@Test
void verifyCurrentCode_true() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
String code = generator.generate(1);
assertThat(generator.verify(code, 1), is(true));
}
@Test
void verifyOlderCodeWithDelayWindowIs0_false() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
String code = generator.generate(1);
assertThat(generator.verify(code, 2), is(false));
}
@Test
void verifyOlderCodeWithDelayWindowIs1_true() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
String code = generator.generate(1);
assertThat(generator.verify(code, 2, 1), is(true));
}
@Test
void withDefaultValues_algorithm() {
HOTPGenerator generator = HOTPGenerator.withDefaultValues(secret.getBytes());
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), is(expected));
}
@Test
void withDefaultValues_passwordLength() {
HOTPGenerator generator = HOTPGenerator.withDefaultValues(secret.getBytes());
int expected = 6;
assertThat(generator.getPasswordLength(), is(expected));
}
@Test
void getURIWithIssuer_doesNotThrow() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
assertDoesNotThrow(() -> {
generator.getURI(10, "issuer");
});
}
@Test
void getURIWithIssuerWithSpace_doesNotThrow() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
assertDoesNotThrow(() -> generator.getURI(10, "issuer with space"));
}
@Test
void getURIWithIssuerWithSpace_doesEscapeIssuer() throws URISyntaxException {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
String url = generator.getURI(10, "issuer with space").toString();
assertThat(url, is("otpauth://hotp/issuer+with+space?digits=6&counter=10&secret=vv3kox7uqj4kyakohmzpph3us4cjimh6f3zknb5c2oobq6v2kiyhm27q&issuer=issuer+with+space&algorithm=SHA1"));
}
@Test
void getURIWithIssuer() throws URISyntaxException {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
URI uri = generator.getURI(10, "issuer");
assertThat(uri.toString(), is("otpauth://hotp/issuer?digits=6&counter=10&secret=" + secret + "&issuer=issuer&algorithm=SHA1"));
}
@Test
void getURIWithIssuerWithUrlUnsafeCharacters() throws URISyntaxException {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
URI uri = generator.getURI(10, "mac&cheese");
assertThat(uri.toString(), is("otpauth://hotp/mac%26cheese?digits=6&counter=10&secret=" + secret + "&issuer=mac%26cheese&algorithm=SHA1"));
}
@Test
void getURIWithIssuerAndAccount_doesNotThrow() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
assertDoesNotThrow(() -> {
generator.getURI(100, "issuer", "account");
});
}
@Test
void getURIWithIssuerAndAccount() throws URISyntaxException {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
URI uri = generator.getURI(100, "issuer", "account");
assertThat(uri.toString(), is("otpauth://hotp/issuer:account?digits=6&counter=100&secret=" + secret + "&issuer=issuer&algorithm=SHA1"));
}
@Test
void getURIWithIssuerAndAccountWithUrlUnsafeCharacters() throws URISyntaxException {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
URI uri = generator.getURI(100, "mac&cheese", "ac@cou.nt");
assertThat(uri.toString(), is("otpauth://hotp/mac%26cheese:ac%40cou.nt?digits=6&counter=100&secret=" + secret + "&issuer=mac%26cheese&algorithm=SHA1"));
}
@Test
void fromURIWithAlgorithmUppercase() throws URISyntaxException {
URI uri = new URI("otpauth://hotp/issuer?counter=10&algorithm=SHA256&secret=" + secret);
HOTPGenerator generator = HOTPGenerator.fromURI(uri);
HMACAlgorithm expected = HMACAlgorithm.SHA256;
assertThat(generator.getAlgorithm(), is(expected));
}
@Test
void fromURIWithAlgorithmLowercase() throws URISyntaxException {
URI uri = new URI("otpauth://hotp/issuer?counter=10&algorithm=sha256&secret=" + secret);
HOTPGenerator generator = HOTPGenerator.fromURI(uri);
HMACAlgorithm expected = HMACAlgorithm.SHA256;
assertThat(generator.getAlgorithm(), is(expected));
}
@Test
void fromURIWithDigitsIs7() throws URISyntaxException {
URI uri = new URI("otpauth://hotp/issuer?digits=7&counter=10&secret=" + secret);
HOTPGenerator generator = HOTPGenerator.fromURI(uri);
int expected = 7;
assertThat(generator.getPasswordLength(), is(expected));
}
@Test
void fromURIWithInvalidDigits_throwsURISyntaxException() throws URISyntaxException {
URI uri = new URI("otpauth://hotp/issuer?digits=invalid&counter=10&secret=" + secret);
assertThrows(URISyntaxException.class, () -> HOTPGenerator.fromURI(uri));
}
@Test
void fromURIWithInvalidAlgorithm_throwsURISyntaxException() throws URISyntaxException {
URI uri = new URI("otpauth://hotp/issuer?counter=10&algorithm=invalid&secret=" + secret);
assertThrows(URISyntaxException.class, () -> HOTPGenerator.fromURI(uri));
}
@Test
void fromURIWithInvalidSecret_throwsIllegalArgumentException() throws URISyntaxException {
URI uri = new URI("otpauth://hotp/issuer?counter=10");
assertThrows(IllegalArgumentException.class, () -> HOTPGenerator.fromURI(uri));
}
@Nested
class BuilderTest {
@Test
void builderWithEmptySecret_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> new HOTPGenerator.Builder(new byte[]{}).build());
}
@Test
void builderWithPasswordLengthIs5_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> {
new HOTPGenerator.Builder(secret).withPasswordLength(5).build();
});
}
@Test
void builderWithPasswordLengthIs9_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> {
new HOTPGenerator.Builder(secret).withPasswordLength(9).build();
});
}
@Test
void builderWithPasswordLengthIs6() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).withPasswordLength(6).build();
int expected = 6;
assertThat(generator.getPasswordLength(), Matchers.is(expected));
}
@Test
void builderWithAlgorithmSHA1() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).withAlgorithm(HMACAlgorithm.SHA1).build();
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), Matchers.is(expected));
}
@Test
void builderWithAlgorithmSHA256() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).withAlgorithm(HMACAlgorithm.SHA256).build();
HMACAlgorithm expected = HMACAlgorithm.SHA256;
assertThat(generator.getAlgorithm(), Matchers.is(expected));
}
@Test
void builderWithAlgorithmSHA512() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).withAlgorithm(HMACAlgorithm.SHA512).build();
HMACAlgorithm expected = HMACAlgorithm.SHA512;
assertThat(generator.getAlgorithm(), Matchers.is(expected));
}
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3, 4, 5, 9, 10 })
void builderWithInvalidPasswordLength_throwsIllegalArgumentException(int passwordLength) {
assertThrows(IllegalArgumentException.class, () -> new HOTPGenerator.Builder(secret).withPasswordLength(passwordLength).build());
}
@Test
void builderWithoutAlgorithm_defaultAlgorithm() {
HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), is(expected));
}
}
}
================================================
FILE: src/test/java/com/bastiaanjansen/otp/SecretGeneratorTest.java
================================================
package com.bastiaanjansen.otp;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class SecretGeneratorTest {
@Test
void generate_defaultLengthIs32() {
int expected = 32;
assertThat(SecretGenerator.generate().length, is(expected));
}
@Test
void generate_lengthIs56() {
int expected = 56;
assertThat(SecretGenerator.generate(256).length, is(expected));
}
@Test
void generateWithZeroBits_throwsIllegalArgument() {
assertThrows(IllegalArgumentException.class, () -> SecretGenerator.generate(0));
}
@Test
void generateWithLessThanZeroBits_throwsIllegalArgument() {
assertThrows(IllegalArgumentException.class, () -> SecretGenerator.generate(-1));
}
}
================================================
FILE: src/test/java/com/bastiaanjansen/otp/TOTPGeneratorTest.java
================================================
package com.bastiaanjansen.otp;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Date;
import java.util.stream.Stream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
class TOTPGeneratorTest {
private final static String secret = "vv3kox7uqj4kyakohmzpph3us4cjimh6f3zknb5c2oobq6v2kiyhm27q";
private static Stream<Arguments> secondsPast1970TestData() {
return Stream.of(
Arguments.of(6, 1, HMACAlgorithm.SHA1, "455216"),
Arguments.of(7, 1, HMACAlgorithm.SHA1, "7455216"),
Arguments.of(8, 1, HMACAlgorithm.SHA1, "17455216"),
Arguments.of(6, 1000, HMACAlgorithm.SHA1, "687469"),
Arguments.of(6, 923892, HMACAlgorithm.SHA1, "909546"),
Arguments.of(6, 82764924, HMACAlgorithm.SHA1, "408978"),
Arguments.of(6, 1, HMACAlgorithm.SHA256, "755370"),
Arguments.of(6, 1, HMACAlgorithm.SHA512, "303161")
);
}
private static Stream<Arguments> instantTestData() {
return Stream.of(
Arguments.of(6, Instant.ofEpochSecond(1), HMACAlgorithm.SHA1, "455216"),
Arguments.of(7, Instant.ofEpochSecond(1000), HMACAlgorithm.SHA1, "0687469"),
Arguments.of(8, Instant.ofEpochSecond(923892), HMACAlgorithm.SHA1, "39909546"),
Arguments.of(6, Instant.ofEpochSecond(82764924), HMACAlgorithm.SHA256, "999993"),
Arguments.of(6, Instant.ofEpochSecond(82764924), HMACAlgorithm.SHA512, "300089")
);
}
private static Stream<Arguments> dateTestData() {
return Stream.of(
Arguments.of(6, Date.from(Instant.ofEpochSecond(1)), HMACAlgorithm.SHA1, "455216"),
Arguments.of(7, Date.from(Instant.ofEpochSecond(100)), HMACAlgorithm.SHA1, "9650012"),
Arguments.of(8, Date.from(Instant.ofEpochSecond(723)), HMACAlgorithm.SHA1, "12251322"),
Arguments.of(6, Date.from(Instant.ofEpochSecond(123)), HMACAlgorithm.SHA256, "376047"),
Arguments.of(6, Date.from(Instant.ofEpochSecond(9802467)), HMACAlgorithm.SHA512, "040816")
);
}
private static Stream<Arguments> clockTestData() {
return Stream.of(
Arguments.of(6, Clock.fixed(Instant.ofEpochSecond(1), ZoneId.of("UTC")), HMACAlgorithm.SHA1, "455216"),
Arguments.of(7, Clock.fixed(Instant.ofEpochSecond(100), ZoneId.of("UTC")), HMACAlgorithm.SHA1, "9650012"),
Arguments.of(8, Clock.fixed(Instant.ofEpochSecond(723), ZoneId.of("UTC")), HMACAlgorithm.SHA1, "12251322"),
Arguments.of(6, Clock.fixed(Instant.ofEpochSecond(123), ZoneId.of("UTC")), HMACAlgorithm.SHA256, "376047"),
Arguments.of(6, Clock.fixed(Instant.ofEpochSecond(9802467), ZoneId.of("UTC")), HMACAlgorithm.SHA512, "040816")
);
}
@ParameterizedTest
@MethodSource("secondsPast1970TestData")
void generateAtSecondsPast1970(int passwordLength, int secondsPast1970, HMACAlgorithm algorithm, String otp) {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {
builder.withPasswordLength(passwordLength);
builder.withAlgorithm(algorithm);
}).build();
assertThat(generator.at(secondsPast1970), is(otp));
}
@ParameterizedTest
@MethodSource("instantTestData")
void generateAtInstant(int passwordLength, Instant instant, HMACAlgorithm algorithm, String otp) {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {
builder.withPasswordLength(passwordLength);
builder.withAlgorithm(algorithm);
}).build();
assertThat(generator.at(instant), is(otp));
}
@ParameterizedTest
@MethodSource("dateTestData")
void generateAtDate(int passwordLength, Date date, HMACAlgorithm algorithm, String otp) {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {
builder.withPasswordLength(passwordLength);
builder.withAlgorithm(algorithm);
}).build();
assertThat(generator.at(date), is(otp));
}
@ParameterizedTest
@MethodSource("clockTestData")
void generateAtNow(int passwordLength, Clock clock, HMACAlgorithm algorithm, String otp) {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {
builder.withPasswordLength(passwordLength);
builder.withAlgorithm(algorithm);
})
.withClock(clock)
.build();
assertThat(generator.now(), is(otp));
}
@ParameterizedTest
@ValueSource(ints = {0, -1})
void generateWithInvalidSecondsPast1970_throwsIllegalArgumentException(int secondsPast1970) {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
assertThrows(IllegalArgumentException.class, () -> generator.at(secondsPast1970));
}
@Test
void verifyCurrentCode_true() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
String code = generator.now();
assertThat(generator.verify(code), is(true));
}
@Test
void verifyOlderCodeWithDelayWindowIs0_false() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
String code = generator.at(Instant.now().minusSeconds(30));
assertThat(generator.verify(code), is(false));
}
@Test
void verifyOlderCodeWithDelayWindowIs1_true() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
String code = generator.at(Instant.now().minusSeconds(30));
assertThat(generator.verify(code, 1), is(true));
}
@Test
void getURIWithIssuer() throws URISyntaxException {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
URI uri = generator.getURI("issuer");
assertThat(uri.toString(), is("otpauth://totp/issuer?period=30&digits=6&secret=" + secret + "&issuer=issuer&algorithm=SHA1"));
}
@Test
void getURIWithIssuerWithUrlUnsafeCharacters() throws URISyntaxException {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
URI uri = generator.getURI("mac&cheese");
assertThat(uri.toString(), is("otpauth://totp/mac%26cheese?period=30&digits=6&secret=" + secret + "&issuer=mac%26cheese&algorithm=SHA1"));
}
@Test
void getURIWithIssuerAndAccount_doesNotThrow() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
assertDoesNotThrow(() -> {
generator.getURI("issuer", "account");
});
}
@Test
void getURIWithIssuerAndAccount() throws URISyntaxException {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
URI uri = generator.getURI("issuer", "account");
assertThat(uri.toString(), is("otpauth://totp/issuer:account?period=30&digits=6&secret=" + secret + "&issuer=issuer&algorithm=SHA1"));
}
@Test
void getURIWithIssuerAndAccountWithUrlUnsafeCharacters() throws URISyntaxException {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
URI uri = generator.getURI("mac&cheese", "ac@cou.nt");
assertThat(uri.toString(), is("otpauth://totp/mac%26cheese:ac%40cou.nt?period=30&digits=6&secret=" + secret + "&issuer=mac%26cheese&algorithm=SHA1"));
}
@Test
void fromURIWithPeriod() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?period=60&secret=" + secret);
TOTPGenerator generator = TOTPGenerator.fromURI(uri);
Duration expected = Duration.ofSeconds(60);
assertThat(generator.getPeriod(), is(expected));
}
@Test
void fromURIWithAlgorithmUppercase() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?algorithm=SHA1&secret=" + secret);
TOTPGenerator generator = TOTPGenerator.fromURI(uri);
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), is(expected));
}
@Test
void fromURIWithAlgorithmLowercase() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?algorithm=sha1&secret=" + secret);
TOTPGenerator generator = TOTPGenerator.fromURI(uri);
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), is(expected));
}
@Test
void fromURIWithPasswordLength() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?digits=6&secret=" + secret);
TOTPGenerator generator = TOTPGenerator.fromURI(uri);
int expected = 6;
assertThat(generator.getPasswordLength(), is(expected));
}
@Test
void fromURIWithInvalidPeriod_throwsURISyntaxException() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?period=invalid&secret=" + secret);
assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));
}
@Test
void fromURIWithPasswordLengthIs5_throwsURISyntaxException() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?digits=5&secret=" + secret);
assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));
}
@Test
void fromURIWithPasswordLengthIs9_throwsURISyntaxException() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?digits=9&secret=" + secret);
assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));
}
@ParameterizedTest
@ValueSource(strings = {
"otpauth://totp/issuer:account?digits=6&secret=",
"otpauth://totp/issuer:account?digits=8&secret=",
"otpauth://totp/issuer:account?digits=7&secret="
})
void fromURI_doesNotThrow(String url) throws URISyntaxException {
URI uri = new URI(url + secret);
assertDoesNotThrow(() -> {
TOTPGenerator.fromURI(uri);
});
}
@Test
void fromURIWithInvalidAlgorithm_throwsURISyntaxException() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?algorithm=invalid&secret=" + secret);
assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));
}
@Nested
class BuilderTest {
@Test
void builderWithEmptySecret_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> new TOTPGenerator.Builder(new byte[]{}).build());
}
@Test
void builderWithPasswordLengthIs5_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> {
new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(5)).build();
});
}
@Test
void builderWithPasswordLengthIs9_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> {
new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(9)).build();
});
}
@Test
void builderWithPasswordLengthIs6() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(6)).build();
int expected = 6;
assertThat(generator.getPasswordLength(), Matchers.is(expected));
}
@Test
void builderWithAlgorithmSHA1() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withAlgorithm(HMACAlgorithm.SHA1)).build();
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), Matchers.is(expected));
}
@Test
void builderWithAlgorithmSHA256() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withAlgorithm(HMACAlgorithm.SHA256)).build();
HMACAlgorithm expected = HMACAlgorithm.SHA256;
assertThat(generator.getAlgorithm(), Matchers.is(expected));
}
@Test
void builderWithAlgorithmSHA512() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withAlgorithm(HMACAlgorithm.SHA512)).build();
HMACAlgorithm expected = HMACAlgorithm.SHA512;
assertThat(generator.getAlgorithm(), Matchers.is(expected));
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 9, 10})
void builderWithInvalidPasswordLength_throwsIllegalArgumentException(int passwordLength) {
assertThrows(IllegalArgumentException.class, () -> new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(passwordLength)).build());
}
@Test
void builderWithoutPeriod_defaultPeriod() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
Duration expected = Duration.ofSeconds(30);
assertThat(generator.getPeriod(), is(expected));
}
@Test
void builderWithoutAlgorithm_defaultAlgorithm() {
TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();
HMACAlgorithm expected = HMACAlgorithm.SHA1;
assertThat(generator.getAlgorithm(), is(expected));
}
}
}
================================================
FILE: src/test/java/com/bastiaanjansen/otp/helpers/URIHelperTest.java
================================================
package com.bastiaanjansen.otp.helpers;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
class URIHelperTest {
@Test
void queryItemsWithOneQueryItem() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?algorithm=SHA1");
Map<String, String> query = URIHelper.queryItems(uri);
String expected = "SHA1";
assertThat(query.get("algorithm"), is(expected));
}
@Test
void queryItemsWithTwoQueryItems() throws URISyntaxException {
URI uri = new URI("otpauth://totp/issuer:account?algorithm=SHA1&secret=ABC");
Map<String, String> query = URIHelper.queryItems(uri);
String expected = "ABC";
assertThat(query.get("secret"), is(expected));
}
@Test
void createURI_doesNotThrow() {
Map<String, String> query = new HashMap<>();
assertDoesNotThrow(() -> URIHelper.createURI("scheme", "host", "path", query));
}
@Test
void createURIWithOneQueryItem() throws URISyntaxException {
Map<String, String> query = new HashMap<>();
query.put("test", "1");
URI uri = URIHelper.createURI("scheme", "host", "path", query);
String expected = "scheme://host/path?test=1";
assertThat(uri.toString(), is(expected));
}
@Test
void createURIWithTwoQueryItems() throws URISyntaxException {
Map<String, String> query = new HashMap<>();
query.put("test", "1");
query.put("test2", "2");
URI uri = URIHelper.createURI("scheme", "host", "path", query);
String expected = "scheme://host/path?test2=2&test=1";
assertThat(uri.toString(), is(expected));
}
@Test
void createURIWithUrlUnsafeCharacters() throws URISyntaxException {
Map<String, String> query = new HashMap<>();
query.put("test", "value 1");
query.put("test2", "value?&2");
URI uri = URIHelper.createURI("scheme", "host", "path", query);
String expected = "scheme://host/path?test2=value%3F%262&test=value+1";
assertThat(uri.toString(), is(expected));
}
}
gitextract_cwl01tct/
├── .github/
│ └── workflows/
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src/
├── main/
│ └── java/
│ └── com/
│ └── bastiaanjansen/
│ └── otp/
│ ├── HMACAlgorithm.java
│ ├── HOTPGenerator.java
│ ├── SecretGenerator.java
│ ├── TOTPGenerator.java
│ └── helpers/
│ └── URIHelper.java
└── test/
└── java/
└── com/
└── bastiaanjansen/
└── otp/
├── ExampleApp.java
├── HOTPGeneratorTest.java
├── SecretGeneratorTest.java
├── TOTPGeneratorTest.java
└── helpers/
└── URIHelperTest.java
SYMBOL INDEX (150 symbols across 10 files)
FILE: src/main/java/com/bastiaanjansen/otp/HMACAlgorithm.java
type HMACAlgorithm (line 7) | public enum HMACAlgorithm {
method HMACAlgorithm (line 18) | HMACAlgorithm(String name) {
method getHMACName (line 22) | public String getHMACName() {
FILE: src/main/java/com/bastiaanjansen/otp/HOTPGenerator.java
class HOTPGenerator (line 19) | public final class HOTPGenerator {
method HOTPGenerator (line 32) | private HOTPGenerator(final Builder builder) {
method fromURI (line 38) | public static HOTPGenerator fromURI(final URI uri) throws URISyntaxExc...
method withDefaultValues (line 62) | public static HOTPGenerator withDefaultValues(final byte[] secret) {
method getURI (line 66) | public URI getURI(final int counter, final String issuer) throws URISy...
method getURI (line 70) | public URI getURI(final int counter, final String issuer, final String...
method getPasswordLength (line 77) | public int getPasswordLength() {
method getAlgorithm (line 81) | public HMACAlgorithm getAlgorithm() {
method verify (line 85) | public boolean verify(final String code, final long counter) {
method verify (line 89) | public boolean verify(final String code, final long counter, final int...
method generate (line 100) | public String generate(final long counter) throws IllegalStateException {
method getURI (line 118) | public URI getURI(final String type, final String issuer, final String...
method decodeBase32 (line 135) | private byte[] decodeBase32(final byte[] value) {
method longToBytes (line 140) | private byte[] longToBytes(final long value) {
method generateHash (line 144) | private byte[] generateHash(final byte[] secret, final byte[] data) th...
method getCodeFromHash (line 154) | private String getCodeFromHash(final byte[] hash) {
class Builder (line 192) | public static final class Builder {
method Builder (line 213) | public Builder(final byte[] secret) {
method Builder (line 225) | public Builder(String secret) {
method withPasswordLength (line 229) | public Builder withPasswordLength(final int passwordLength) {
method withAlgorithm (line 237) | public Builder withAlgorithm(final HMACAlgorithm algorithm) {
method build (line 242) | public HOTPGenerator build() {
method passwordLengthIsValid (line 246) | private boolean passwordLengthIsValid(final int passwordLength) {
FILE: src/main/java/com/bastiaanjansen/otp/SecretGenerator.java
class SecretGenerator (line 12) | public class SecretGenerator {
method SecretGenerator (line 14) | private SecretGenerator() {}
method generate (line 29) | public static byte[] generate() {
method generate (line 42) | public static byte[] generate(final int bits) {
FILE: src/main/java/com/bastiaanjansen/otp/TOTPGenerator.java
class TOTPGenerator (line 17) | public final class TOTPGenerator {
method TOTPGenerator (line 28) | private TOTPGenerator(final Builder builder) {
method fromURI (line 34) | public static TOTPGenerator fromURI(URI uri) throws URISyntaxException {
method withDefaultValues (line 61) | public static TOTPGenerator withDefaultValues(final byte[] secret) {
method now (line 65) | public String now() throws IllegalStateException {
method now (line 70) | public String now(Clock clock) throws IllegalStateException {
method at (line 75) | public String at(final Instant instant) throws IllegalStateException {
method at (line 79) | public String at(final Date date) throws IllegalStateException {
method at (line 84) | public String at(final LocalDate date) throws IllegalStateException {
method at (line 89) | public String at(final long secondsPast1970) throws IllegalArgumentExc...
method verify (line 97) | public boolean verify(final String code) {
method verify (line 109) | public boolean verify(final String code, final int delayWindow) {
method getURI (line 114) | public URI getURI(final String issuer) throws URISyntaxException {
method getURI (line 118) | public URI getURI(final String issuer, final String account) throws UR...
method durationUntilNextTimeWindow (line 130) | public Duration durationUntilNextTimeWindow() {
method durationUntilNextTimeWindow (line 134) | public Duration durationUntilNextTimeWindow(Clock clock) {
method getPeriod (line 139) | public Duration getPeriod() {
method getClock (line 143) | public Clock getClock() {
method getAlgorithm (line 147) | public HMACAlgorithm getAlgorithm() {
method getPasswordLength (line 151) | public int getPasswordLength() {
method calculateCounter (line 155) | private long calculateCounter(final long secondsPast1970, final Durati...
method calculateCounter (line 159) | private long calculateCounter(final Clock clock, final Duration period) {
method validateTime (line 163) | private boolean validateTime(final long time) {
class Builder (line 167) | public static final class Builder {
method Builder (line 185) | public Builder(byte[] secret) {
method Builder (line 194) | public Builder(String secret) {
method withHOTPGenerator (line 198) | public Builder withHOTPGenerator(Consumer<HOTPGenerator.Builder> bui...
method withClock (line 203) | public Builder withClock(Clock clock) {
method withPeriod (line 208) | public Builder withPeriod(Duration period) {
method build (line 214) | public TOTPGenerator build() {
FILE: src/main/java/com/bastiaanjansen/otp/helpers/URIHelper.java
class URIHelper (line 18) | public class URIHelper {
method URIHelper (line 27) | private URIHelper() {}
method queryItems (line 35) | public static Map<String, String> queryItems(URI uri) {
method createURI (line 64) | public static URI createURI(String scheme, String host, String path, M...
method encode (line 74) | public static String encode(String value) {
FILE: src/test/java/com/bastiaanjansen/otp/ExampleApp.java
class ExampleApp (line 6) | public class ExampleApp {
method main (line 7) | public static void main(String[] args) {
FILE: src/test/java/com/bastiaanjansen/otp/HOTPGeneratorTest.java
class HOTPGeneratorTest (line 20) | class HOTPGeneratorTest {
method testData (line 24) | private static Stream<Arguments> testData() {
method generateWithCounter (line 41) | @ParameterizedTest
method generateWithInvalidCounter_throwsIllegalArgumentException (line 52) | @ParameterizedTest
method verifyCurrentCode_true (line 60) | @Test
method verifyOlderCodeWithDelayWindowIs0_false (line 68) | @Test
method verifyOlderCodeWithDelayWindowIs1_true (line 76) | @Test
method withDefaultValues_algorithm (line 84) | @Test
method withDefaultValues_passwordLength (line 92) | @Test
method getURIWithIssuer_doesNotThrow (line 100) | @Test
method getURIWithIssuerWithSpace_doesNotThrow (line 109) | @Test
method getURIWithIssuerWithSpace_doesEscapeIssuer (line 116) | @Test
method getURIWithIssuer (line 125) | @Test
method getURIWithIssuerWithUrlUnsafeCharacters (line 133) | @Test
method getURIWithIssuerAndAccount_doesNotThrow (line 142) | @Test
method getURIWithIssuerAndAccount (line 151) | @Test
method getURIWithIssuerAndAccountWithUrlUnsafeCharacters (line 159) | @Test
method fromURIWithAlgorithmUppercase (line 168) | @Test
method fromURIWithAlgorithmLowercase (line 178) | @Test
method fromURIWithDigitsIs7 (line 187) | @Test
method fromURIWithInvalidDigits_throwsURISyntaxException (line 196) | @Test
method fromURIWithInvalidAlgorithm_throwsURISyntaxException (line 203) | @Test
method fromURIWithInvalidSecret_throwsIllegalArgumentException (line 210) | @Test
class BuilderTest (line 217) | @Nested
method builderWithEmptySecret_throwsIllegalArgumentException (line 219) | @Test
method builderWithPasswordLengthIs5_throwsIllegalArgumentException (line 224) | @Test
method builderWithPasswordLengthIs9_throwsIllegalArgumentException (line 231) | @Test
method builderWithPasswordLengthIs6 (line 238) | @Test
method builderWithAlgorithmSHA1 (line 246) | @Test
method builderWithAlgorithmSHA256 (line 254) | @Test
method builderWithAlgorithmSHA512 (line 262) | @Test
method builderWithInvalidPasswordLength_throwsIllegalArgumentException (line 270) | @ParameterizedTest
method builderWithoutAlgorithm_defaultAlgorithm (line 276) | @Test
FILE: src/test/java/com/bastiaanjansen/otp/SecretGeneratorTest.java
class SecretGeneratorTest (line 9) | class SecretGeneratorTest {
method generate_defaultLengthIs32 (line 11) | @Test
method generate_lengthIs56 (line 17) | @Test
method generateWithZeroBits_throwsIllegalArgument (line 23) | @Test
method generateWithLessThanZeroBits_throwsIllegalArgument (line 28) | @Test
FILE: src/test/java/com/bastiaanjansen/otp/TOTPGeneratorTest.java
class TOTPGeneratorTest (line 25) | class TOTPGeneratorTest {
method secondsPast1970TestData (line 29) | private static Stream<Arguments> secondsPast1970TestData() {
method instantTestData (line 44) | private static Stream<Arguments> instantTestData() {
method dateTestData (line 54) | private static Stream<Arguments> dateTestData() {
method clockTestData (line 64) | private static Stream<Arguments> clockTestData() {
method generateAtSecondsPast1970 (line 74) | @ParameterizedTest
method generateAtInstant (line 85) | @ParameterizedTest
method generateAtDate (line 96) | @ParameterizedTest
method generateAtNow (line 107) | @ParameterizedTest
method generateWithInvalidSecondsPast1970_throwsIllegalArgumentException (line 121) | @ParameterizedTest
method verifyCurrentCode_true (line 130) | @Test
method verifyOlderCodeWithDelayWindowIs0_false (line 138) | @Test
method verifyOlderCodeWithDelayWindowIs1_true (line 146) | @Test
method getURIWithIssuer (line 155) | @Test
method getURIWithIssuerWithUrlUnsafeCharacters (line 163) | @Test
method getURIWithIssuerAndAccount_doesNotThrow (line 171) | @Test
method getURIWithIssuerAndAccount (line 180) | @Test
method getURIWithIssuerAndAccountWithUrlUnsafeCharacters (line 189) | @Test
method fromURIWithPeriod (line 198) | @Test
method fromURIWithAlgorithmUppercase (line 208) | @Test
method fromURIWithAlgorithmLowercase (line 218) | @Test
method fromURIWithPasswordLength (line 228) | @Test
method fromURIWithInvalidPeriod_throwsURISyntaxException (line 238) | @Test
method fromURIWithPasswordLengthIs5_throwsURISyntaxException (line 245) | @Test
method fromURIWithPasswordLengthIs9_throwsURISyntaxException (line 252) | @Test
method fromURI_doesNotThrow (line 259) | @ParameterizedTest
method fromURIWithInvalidAlgorithm_throwsURISyntaxException (line 273) | @Test
class BuilderTest (line 280) | @Nested
method builderWithEmptySecret_throwsIllegalArgumentException (line 282) | @Test
method builderWithPasswordLengthIs5_throwsIllegalArgumentException (line 287) | @Test
method builderWithPasswordLengthIs9_throwsIllegalArgumentException (line 294) | @Test
method builderWithPasswordLengthIs6 (line 301) | @Test
method builderWithAlgorithmSHA1 (line 309) | @Test
method builderWithAlgorithmSHA256 (line 317) | @Test
method builderWithAlgorithmSHA512 (line 325) | @Test
method builderWithInvalidPasswordLength_throwsIllegalArgumentException (line 333) | @ParameterizedTest
method builderWithoutPeriod_defaultPeriod (line 339) | @Test
method builderWithoutAlgorithm_defaultAlgorithm (line 347) | @Test
FILE: src/test/java/com/bastiaanjansen/otp/helpers/URIHelperTest.java
class URIHelperTest (line 14) | class URIHelperTest {
method queryItemsWithOneQueryItem (line 16) | @Test
method queryItemsWithTwoQueryItems (line 25) | @Test
method createURI_doesNotThrow (line 34) | @Test
method createURIWithOneQueryItem (line 41) | @Test
method createURIWithTwoQueryItems (line 51) | @Test
method createURIWithUrlUnsafeCharacters (line 62) | @Test
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (72K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 1322,
"preview": "name: Build & Test\n\non:\n push:\n branches: [ main ]\n pull_request:\n branches: [ main ]\n\njobs:\n build:\n name: "
},
{
"path": ".github/workflows/publish.yml",
"chars": 2538,
"preview": "name: Maven Package\n\non:\n release:\n types: [ created ]\n\njobs:\n build:\n runs-on: ubuntu-latest\n strategy:\n "
},
{
"path": ".gitignore",
"chars": 73,
"preview": ".idea\ntarget\n.m2\npom.xml.releaseBackup\nrelease.properties\n.DS_Store\n*.iml"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2020 Bastiaan Jansen\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 7336,
"preview": "# OTP-Java\n\n[. The extraction includes 16 files (67.3 KB), approximately 16.4k tokens, and a symbol index with 150 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.