[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build & Test\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  build:\n    name: Java ${{ matrix.java }} build\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v3\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-package: jdk\n          java-version: ${{ matrix.java }}\n      - name: Build with Maven\n        run: mvn -B package --file pom.xml\n      - run: mkdir artifacts && cp target/*.jar artifacts\n      - name: Upload Maven build artifact\n        uses: actions/upload-artifact@v3\n        with:\n          name: build-java-${{ matrix.java }}.jar\n          path: artifacts\n\n  test:\n    name: Java ${{ matrix.java }} test\n    needs: [build]\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v3\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: ${{ matrix.java }}\n      - name: Run tests with Maven\n        run: mvn -B test --file pom.xml\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Maven Package\n\non:\n  release:\n    types: [ created ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]\n    name: Java ${{ matrix.java }} build\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v3\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: ${{ matrix.java }}\n      - name: Build with Maven\n        run: mvn -B package --file pom.xml\n      - run: mkdir artifacts && cp target/*.jar artifacts\n      - name: Upload Maven build artifact\n        uses: actions/upload-artifact@v3\n        with:\n          name: build-java-${{ matrix.java }}.jar\n          path: artifacts\n\n  test:\n    needs: [ build ]\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java: [ 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]\n    name: Java ${{ matrix.java }} test\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v3\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: ${{ matrix.java }}\n      - name: Run tests with Maven\n        run: mvn -B test --file pom.xml\n\n  publish-maven-central:\n    name: Publish to Maven Central Repository\n    needs: [ build, test ]\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Java\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'adopt'\n      - name: Publish package\n        uses: samuelmeuli/action-maven-publish@v1\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }}\n          nexus_username: ${{ secrets.NEXUS_USERNAME }}\n          nexus_password: ${{ secrets.NEXUS_PASSWORD }}\n          maven_profiles: \"deploy-maven-central\"\n\n  publish-github-registry:\n    name: Publish to GitHub Registry\n    needs: [ build, test ]\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-java@v3\n        with:\n          server-id: github\n          java-version: '11'\n          distribution: 'adopt'\n      - name: Publish package\n        run: mvn --batch-mode --activate-profiles \"deploy-github-registry\" deploy\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\ntarget\n.m2\npom.xml.releaseBackup\nrelease.properties\n.DS_Store\n*.iml"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Bastiaan Jansen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# OTP-Java\n\n[![Build & Test](https://github.com/BastiaanJansen/otp-java/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/BastiaanJansen/otp-java/actions/workflows/build.yml)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/91d3addee9e94a0cad9436601d4a4e1e)](https://www.codacy.com/gh/BastiaanJansen/OTP-Java/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=BastiaanJansen/OTP-Java&amp;utm_campaign=Badge_Grade)\n![](https://img.shields.io/github/license/BastiaanJansen/OTP-Java)\n![](https://img.shields.io/github/issues/BastiaanJansen/OTP-Java)\n\nA 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).\n\n## Table of Contents\n\n* [Features](#features)\n* [Installation](#installation)\n* [Usage](#usage)\n    * [HOTP (Counter-based one-time passwords)](#counter-based-one-time-passwords)\n    * [TOTP (Time-based one-time passwords)](#time-based-one-time-passwords)\n    * [Recovery codes](#recovery-codes)\n\n## Features\nThe following features are supported:\n1. Generation of secrets\n2. Time-based one-time password (TOTP, RFC 6238) generation based on current time, specific time, OTPAuth URI and more for different HMAC algorithms.\n3. HMAC-based one-time password (HOTP, RFC 4226) generation based on counter and OTPAuth URI.\n4. Verification of one-time passwords\n5. Generation of OTP Auth URI's\n\n## Installation\n### Maven\n```xml\n<dependency>\n    <groupId>com.github.bastiaanjansen</groupId>\n    <artifactId>otp-java</artifactId>\n    <version>2.1.0</version>\n</dependency>\n```\n\n### Gradle\n```gradle\nimplementation 'com.github.bastiaanjansen:otp-java:2.1.0'\n```\n\n### Scala SBT\n```scala\nlibraryDependencies += \"com.github.bastiaanjansen\" % \"otp-java\" % \"2.1.0\"\n```\n\n### Apache Ivy\n```xml\n<dependency org=\"com.github.bastiaanjansen\" name=\"otp-java\" rev=\"2.1.0\" />\n```\n\nOr you can download the source from the [GitHub releases page](https://github.com/BastiaanJansen/OTP-Java/releases).\n\n## Usage\n### HOTP (Counter-based one-time passwords)\n#### Initialization HOTP instance\nTo create a `HOTPGenerator` instance, use the `HOTPGenerator.Builder` class as follows:\n\n```java\nString secret = \"VV3KOX7UQJ4KYAKOHMZPPH3US4CJIMH6F3ZKNB5C2OOBQ6V2KIYHM27Q\";\nHOTPGenerator hotp = new HOTPGenerator.Builder(secret).build();\n```\nThe above builder creates a HOTP instance with default values for passwordLength = 6 and algorithm = SHA1. Use the builder to change these defaults:\n```java\nHOTPGenerator hotp = new HOTPGenerator.Builder(secret)\n        .withPasswordLength(8)\n        .withAlgorithm(HMACAlgorithm.SHA256)\n        .build();\n```\n\nIf you have a shared secret described in [RFC-4226](https://www.rfc-editor.org/rfc/rfc4226), you need to encode it first:\n\n```java\nbyte[] sharedSecret = getMySharedSecret();\n\nbyte[] secret = Base32.encode(sharedSecret);\n```\n\nWhen you don't already have a secret, you can let the library generate it:\n```java\n// To generate a Base32-encoded secret with 160 bits\nbyte[] secret = SecretGenerator.generate();\n\n// To generate a Base32-encoded secret with a custom amount of bits\nbyte[] secret = SecretGenerator.generate(512);\n```\n\nIt 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.\n```java\nURI uri = new URI(\"otpauth://hotp/issuer?secret=ABCDEFGHIJKLMNOP&algorithm=SHA1&digits=6&counter=8237\");\nHOTPGenerator hotp = HOTPGenerator.fromURI(uri);\n```\n\nGet information about the generator:\n\n```java\nint passwordLength = hotp.getPasswordLength(); // 6\nHMACAlgorithm algorithm = hotp.getAlgorithm(); // HMACAlgorithm.SHA1\n```\n\n#### Generation of HOTP code\nAfter creating an instance of the HOTP class, a code can be generated by using the `generate()` method:\n```java\ntry {\n    int counter = 5;\n    String code = hotp.generate(counter);\n    \n    // To verify a token:\n    boolean isValid = hotp.verify(code, counter);\n    \n    // Or verify with a delay window\n    boolean isValid = hotp.verify(code, counter, 2);\n} catch (IllegalStateException e) {\n    // Handle error\n}\n```\n\n### TOTP (Time-based one-time passwords)\n#### Initialization TOTP instance\nTOTP can accept more paramaters: `passwordLength`, `period`, `algorithm` and `secret`. The default values are: passwordLength = 6, period = 30 and algorithm = SHA1.\n\n```java\n// Generate a secret (or use your own secret)\nbyte[] secret = SecretGenerator.generate();\n\nTOTPGenerator totp = new TOTPGenerator.Builder(secret)\n        .withHOTPGenerator(builder -> {\n            builder.withPasswordLength(6);\n            builder.withAlgorithm(HMACAlgorithm.SHA1); // SHA256 and SHA512 are also supported\n        })\n        .withPeriod(Duration.ofSeconds(30))\n        .build();\n```\nOr create a `TOTP` instance from an OTPAuth URI:\n```java\nURI uri = new URI(\"otpauth://totp/issuer?secret=ABCDEFGHIJKLMNOP&algorithm=SHA1&digits=6&period=30\");\nTOTPGenerator totpGenerator = TOTPGenerator.fromURI(uri);\n```\n\nGet information about the generator:\n```java\nint passwordLength = totpGenerator.getPasswordLength(); // 6\nHMACAlgorithm algorithm = totpGenerator.getAlgorithm(); // HMACAlgorithm.SHA1\nDuration period = totpGenerator.getPeriod(); // Duration.ofSeconds(30)\n```\n\n#### Generation of TOTP code\nAfter creating an instance of the TOTP class, a code can be generated by using the `now()` method, similarly with the HOTP class:\n```java\ntry {\n    String code = totpGenerator.now();\n     \n    // To verify a token:\n    boolean isValid = totpGenerator.verify(code);\n} catch (IllegalStateException e) {\n    // Handle error\n}\n```\nThe 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`:\n\n```java\ntry {\n    // Based on current time\n    totpGenerator.now();\n    \n    // Based on specific date\n    totpGenerator.at(new Date());\n    \n    // Based on specific local date\n    totpGenerator.at(LocalDate.of(2023, 3, 2));\n    \n    // Based on seconds past 1970\n    totpGenerator.at(9238346823);\n    \n    // Based on an instant\n    totpGenerator.at(Instant.now());\n} catch (IllegalStateException e) {\n    // Handle error\n}\n```\n\n### Generation of OTPAuth URI's\nTo easily generate a OTPAuth URI for easy on-boarding, use the `getURI()` method for both `HOTP` and `TOTP`. Example for `TOTP`:\n```java\nTOTPGenerator totpGenerator = new TOTPGenerator.Builder(secret).build();\n\nURI uri = totpGenerator.getURI(\"issuer\", \"account\"); // otpauth://totp/issuer:account?period=30&digits=6&secret=SECRET&algorithm=SHA1\n\n```\n\n## Recovery Codes\nOften, 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. \n\nBecause 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.\n\n## Licence\nOTP-Java is available under the MIT License. See the LICENCE for more info.\n\n[![Stargazers repo roster for @BastiaanJansen/otp-java](https://reporoster.com/stars/BastiaanJansen/otp-java)](https://github.com/BastiaanJansen/otp-java/stargazers)\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.sonatype.oss</groupId>\n        <artifactId>oss-parent</artifactId>\n        <version>9</version>\n    </parent>\n\n    <groupId>com.github.bastiaanjansen</groupId>\n    <artifactId>otp-java</artifactId>\n    <version>2.1.0</version>\n\n    <name>OTP-Java</name>\n    <description>A small and easy-to-use one-time password generator for Java according to RFC 4226 (HOTP) and RFC 6238 (TOTP).</description>\n\n    <distributionManagement>\n        <snapshotRepository>\n            <id>ossrh</id>\n            <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n        </snapshotRepository>\n        <repository>\n            <id>ossrh</id>\n            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n        </repository>\n    </distributionManagement>\n\n    <licenses>\n        <license>\n            <name>MIT License</name>\n            <url>https://github.com/BastiaanJansen/otp-java/blob/main/LICENSE</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <properties>\n        <maven.compiler.source>1.8</maven.compiler.source>\n        <maven.compiler.target>1.8</maven.compiler.target>\n    </properties>\n\n    <scm>\n        <connection>scm:git:https://github.com/BastiaanJansen/otp-java.git</connection>\n        <url>http://github.com/BastiaanJansen/otp-java</url>\n        <developerConnection>scm:git:https://github.com/BastiaanJansen/otp-java.git</developerConnection>\n    </scm>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <version>3.10.1</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <version>5.9.0</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>commons-codec</groupId>\n            <artifactId>commons-codec</artifactId>\n            <version>1.15</version>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>java-hamcrest</artifactId>\n            <version>2.0.0.0</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <profiles>\n        <!-- Deployment profile (required so these plugins are only used when deploying) -->\n        <profile>\n            <id>deploy-maven-central</id>\n            <distributionManagement>\n                <snapshotRepository>\n                    <id>ossrh</id>\n                    <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>\n                </snapshotRepository>\n                <repository>\n                    <id>ossrh</id>\n                    <url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n                </repository>\n            </distributionManagement>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.sonatype.plugins</groupId>\n                        <artifactId>nexus-staging-maven-plugin</artifactId>\n                        <version>1.6.8</version>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <serverId>ossrh</serverId>\n                            <nexusUrl>https://oss.sonatype.org/</nexusUrl>\n                            <autoReleaseAfterClose>true</autoReleaseAfterClose>\n                        </configuration>\n                    </plugin>\n                    <!-- Source plugin -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-source-plugin</artifactId>\n                        <version>2.4</version>\n                        <executions>\n                            <execution>\n                                <id>attach-sources</id>\n                                <goals>\n                                    <goal>jar-no-fork</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n\n                    <!-- Javadoc plugin -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <version>2.10.4</version>\n                        <executions>\n                            <execution>\n                                <id>attach-javadocs</id>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n\n                    <!-- GPG plugin -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <version>1.6</version>\n                        <executions>\n                            <execution>\n                                <id>sign-artifacts</id>\n                                <phase>verify</phase>\n                                <goals>\n                                    <goal>sign</goal>\n                                </goals>\n                                <configuration>\n                                    <!-- Prevent `gpg` from using pinentry programs -->\n                                    <gpgArguments>\n                                        <arg>--pinentry-mode</arg>\n                                        <arg>loopback</arg>\n                                    </gpgArguments>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n\n        <profile>\n            <id>deploy-github-registry</id>\n            <distributionManagement>\n                <repository>\n                    <id>github</id>\n                    <name>GitHub Packages</name>\n                    <url>https://maven.pkg.github.com/BastiaanJansen/otp-java</url>\n                </repository>\n            </distributionManagement>\n        </profile>\n    </profiles>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>2.22.2</version>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "src/main/java/com/bastiaanjansen/otp/HMACAlgorithm.java",
    "content": "package com.bastiaanjansen.otp;\r\n\r\n/**\r\n * HMAC algorithm enum\r\n * @author Bastiaan Jansen\r\n */\r\npublic enum HMACAlgorithm {\r\n\r\n    @Deprecated\r\n    SHA1(\"HmacSHA1\"),\r\n    SHA224(\"HmacSHA224\"),\r\n    SHA256(\"HmacSHA256\"),\r\n    SHA384(\"HmacSHA384\"),\r\n    SHA512(\"HmacSHA512\");\r\n\r\n    private final String name;\r\n\r\n    HMACAlgorithm(String name) {\r\n        this.name = name;\r\n    }\r\n\r\n    public String getHMACName() {\r\n        return name;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/bastiaanjansen/otp/HOTPGenerator.java",
    "content": "package com.bastiaanjansen.otp;\n\nimport com.bastiaanjansen.otp.helpers.URIHelper;\nimport org.apache.commons.codec.binary.Base32;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.ByteBuffer;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic final class HOTPGenerator {\n\n    private static final String URL_SCHEME = \"otpauth\";\n    private static final int DEFAULT_PASSWORD_LENGTH = 6;\n    private static final HMACAlgorithm DEFAULT_HMAC_ALGORITHM = HMACAlgorithm.SHA1;\n    private static final String OTP_TYPE = \"hotp\";\n\n    private final int passwordLength;\n\n    private final HMACAlgorithm algorithm;\n\n    private final byte[] secret;\n\n    private HOTPGenerator(final Builder builder) {\n        this.passwordLength = builder.passwordLength;\n        this.algorithm = builder.algorithm;\n        this.secret = builder.secret;\n    }\n\n    public static HOTPGenerator fromURI(final URI uri) throws URISyntaxException {\n        Map<String, String> query = URIHelper.queryItems(uri);\n\n        byte[] secret = Optional.ofNullable(query.get(URIHelper.SECRET))\n                .map(s -> s.getBytes(UTF_8))\n                .orElseThrow(() -> new IllegalArgumentException(\"Secret query parameter must be set\"));\n\n        Builder builder = new Builder(secret);\n\n        try {\n            Optional.ofNullable(query.get(URIHelper.DIGITS))\n                    .map(Integer::valueOf)\n                    .ifPresent(builder::withPasswordLength);\n            Optional.ofNullable(query.get(URIHelper.ALGORITHM))\n                    .map(String::toUpperCase)\n                    .map(HMACAlgorithm::valueOf)\n                    .ifPresent(builder::withAlgorithm);\n        } catch (Exception e) {\n            throw new URISyntaxException(uri.toString(), \"URI could not be parsed\");\n        }\n\n        return builder.build();\n    }\n\n    public static HOTPGenerator withDefaultValues(final byte[] secret) {\n        return new HOTPGenerator.Builder(secret).build();\n    }\n\n    public URI getURI(final int counter, final String issuer) throws URISyntaxException {\n        return getURI(counter, issuer, \"\");\n    }\n\n    public URI getURI(final int counter, final String issuer, final String account) throws URISyntaxException {\n        Map<String, String> query = new HashMap<>();\n        query.put(URIHelper.COUNTER, String.valueOf(counter));\n\n        return getURI(OTP_TYPE, issuer, account, query);\n    }\n\n    public int getPasswordLength() {\n        return passwordLength;\n    }\n\n    public HMACAlgorithm getAlgorithm() {\n        return algorithm;\n    }\n\n    public boolean verify(final String code, final long counter) {\n        return verify(code, counter, 0);\n    }\n\n    public boolean verify(final String code, final long counter, final int delayWindow) {\n        if (code.length() != passwordLength) return false;\n\n        for (int i = -delayWindow; i <= delayWindow; i++) {\n            String currentCode = generate(counter + i);\n            if (code.equals(currentCode)) return true;\n        }\n\n        return false;\n    }\n\n    public String generate(final long counter) throws IllegalStateException {\n        if (counter < 0)\n            throw new IllegalArgumentException(\"Counter must be greater than or equal to 0\");\n\n        byte[] secretBytes = decodeBase32(secret);\n        byte[] counterBytes = longToBytes(counter);\n\n        byte[] hash;\n\n        try {\n            hash = generateHash(secretBytes, counterBytes);\n        } catch (NoSuchAlgorithmException | InvalidKeyException e) {\n            throw new IllegalStateException();\n        }\n\n        return getCodeFromHash(hash);\n    }\n\n    public URI getURI(final String type, final String issuer, final String account, final Map<String, String> query) throws URISyntaxException {\n        query.put(URIHelper.DIGITS, String.valueOf(passwordLength));\n        query.put(URIHelper.ALGORITHM, algorithm.name());\n        query.put(URIHelper.SECRET, new String(secret, UTF_8));\n        query.put(URIHelper.ISSUER, issuer);\n\n        String path = account.isEmpty() ? URIHelper.encode(issuer) : String.format(\"%s:%s\", URIHelper.encode(issuer), URIHelper.encode(account));\n\n        return URIHelper.createURI(URL_SCHEME, type, path, query);\n    }\n\n    /**\n     * Decode a base32 value to bytes array\n     *\n     * @param value base32 value\n     * @return bytes array\n     */\n    private byte[] decodeBase32(final byte[] value) {\n        Base32 codec = new Base32();\n        return codec.decode(value);\n    }\n\n    private byte[] longToBytes(final long value) {\n        return ByteBuffer.allocate(Long.BYTES).putLong(value).array();\n    }\n\n    private byte[] generateHash(final byte[] secret, final byte[] data) throws InvalidKeyException, NoSuchAlgorithmException {\n        // Create a secret key with correct SHA algorithm\n        SecretKeySpec signKey = new SecretKeySpec(secret, \"RAW\");\n        // Mac is 'message authentication code' algorithm (RFC 2104)\n        Mac mac = Mac.getInstance(algorithm.getHMACName());\n        mac.init(signKey);\n        // Hash data with generated sign key\n        return mac.doFinal(data);\n    }\n\n    private String getCodeFromHash(final byte[] hash) {\n        /* Find mask to get last 4 digits:\n        1. Set all bits to 1: ~0 -> 11111111 -> 255 decimal -> 0xFF\n        2. Shift n (in this case 4, because we want the last 4 bits) bits to left with <<\n        3. Negate the result: 1111 1100 -> 0000 0011\n         */\n        int mask = ~(~0 << 4);\n\n        /* Get last 4 bits of hash as offset:\n        Use the bitwise AND (&) operator to select last 4 bits\n        Mask should be 00001111 = 15 = 0xF\n        Last byte of hash & 0xF = last 4 bits:\n        Example:\n        Input: decimal 219 as binary: 11011011 &\n        Mask: decimal 15 as binary:   00001111\n        -----------------------------------------\n        Output: decimal 11 as binary: 00001011\n         */\n        byte lastByte = hash[hash.length - 1];\n        int offset = lastByte & mask;\n\n        // Get 4 bytes from hash from offset to offset + 3\n        byte[] truncatedHashInBytes = { hash[offset], hash[offset + 1], hash[offset + 2], hash[offset + 3] };\n\n        // Wrap in ByteBuffer to convert bytes to long\n        ByteBuffer byteBuffer = ByteBuffer.wrap(truncatedHashInBytes);\n        long truncatedHash = byteBuffer.getInt();\n\n        // Mask most significant bit\n        truncatedHash &= 0x7FFFFFFF;\n\n        // Modulo (%) truncatedHash by 10^passwordLength\n        truncatedHash %= Math.pow(10, passwordLength);\n\n        // Left pad with 0s for an n-digit code\n        return String.format(\"%0\" + passwordLength + \"d\", truncatedHash);\n    }\n\n    public static final class Builder {\n\n        private int passwordLength;\n\n        private HMACAlgorithm algorithm;\n\n        /**\n         * Base32 encoded secret\n         */\n        private final byte[] secret;\n\n        /**\n         * Creates a new builder.\n         * <p>\n         * Use {@link SecretGenerator#generate()} to create a secret.\n         * <p>\n         * If you are using a shared secret from another generator, you would likely need to encode it using\n         * {@link org.apache.commons.codec.binary.Base32#encode(byte[])}}\n         *\n         * @param secret Base32 encoded secret\n         */\n        public Builder(final byte[] secret) {\n            if (secret.length == 0)\n                throw new IllegalArgumentException(\"Secret must not be empty\");\n\n            this.secret = secret;\n            this.passwordLength = DEFAULT_PASSWORD_LENGTH;\n            this.algorithm = DEFAULT_HMAC_ALGORITHM;\n        }\n\n        /**\n         * @param secret Base32 encoded secret\n         */\n        public Builder(String secret) {\n            this(secret.getBytes(UTF_8));\n        }\n\n        public Builder withPasswordLength(final int passwordLength) {\n            if (!passwordLengthIsValid(passwordLength))\n                throw new IllegalArgumentException(\"Password length must be between 6 and 8 digits\");\n\n            this.passwordLength = passwordLength;\n            return this;\n        }\n\n        public Builder withAlgorithm(final HMACAlgorithm algorithm) {\n            this.algorithm = algorithm;\n            return this;\n        }\n\n        public HOTPGenerator build() {\n            return new HOTPGenerator(this);\n        }\n\n        private boolean passwordLengthIsValid(final int passwordLength) {\n            return passwordLength >= 6 && passwordLength <= 8;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/bastiaanjansen/otp/SecretGenerator.java",
    "content": "package com.bastiaanjansen.otp;\n\nimport org.apache.commons.codec.binary.Base32;\n\nimport java.security.SecureRandom;\n\n/**\n * A secret generator to generate OTP secrets\n *\n * @author Bastiaan Jansen\n */\npublic class SecretGenerator {\n\n    private SecretGenerator() {}\n\n    /**\n     * Default amount of bits for secret generation\n     */\n    public static final int DEFAULT_BITS = 160;\n\n    private static final SecureRandom random = new SecureRandom();\n    private static final Base32 encoder = new Base32();\n\n    /**\n     * Generate an OTP base32 secret with default amount of bits\n     *\n     * @return generated secret\n     */\n    public static byte[] generate() {\n        return generate(DEFAULT_BITS);\n    }\n\n    /**\n     * Generate an OTP base32 secret\n     *\n     * @param bits length, this should be greater than or equal to the length of the HMAC algorithm type:\n     *             SHA1: 160 bits\n     *             SHA256: 256 bits\n     *             SHA512: 512 bits\n     * @return generated secret\n     */\n    public static byte[] generate(final int bits) {\n        if (bits <= 0)\n            throw new IllegalArgumentException(\"Bits must be greater than or equal to 0\");\n\n        byte[] bytes = new byte[bits / Byte.SIZE];\n        random.nextBytes(bytes);\n\n        return encoder.encode(bytes);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/bastiaanjansen/otp/TOTPGenerator.java",
    "content": "package com.bastiaanjansen.otp;\n\nimport com.bastiaanjansen.otp.helpers.URIHelper;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.time.*;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic final class TOTPGenerator {\n    private static final String OTP_TYPE = \"totp\";\n    private static final Duration DEFAULT_PERIOD = Duration.ofSeconds(30);\n    private static final Clock DEFAULT_CLOCK = Clock.system(ZoneId.systemDefault());\n\n    private final Duration period;\n\n    private final Clock clock;\n\n    private final HOTPGenerator hotpGenerator;\n\n    private TOTPGenerator(final Builder builder) {\n        this.period = builder.period;\n        this.clock = builder.clock;\n        this.hotpGenerator = builder.hotpBuilder.build();\n    }\n\n    public static TOTPGenerator fromURI(URI uri) throws URISyntaxException {\n        Map<String, String> query = URIHelper.queryItems(uri);\n\n        String secret = Optional.ofNullable(query.get(URIHelper.SECRET))\n                .orElseThrow(() -> new IllegalArgumentException(\"Secret query parameter must be set\"));\n\n        Builder builder = new Builder(secret);\n\n        try {\n            Optional.ofNullable(query.get(URIHelper.PERIOD))\n                    .map(Long::parseLong)\n                    .map(Duration::ofSeconds)\n                    .ifPresent(builder::withPeriod);\n            Optional.ofNullable(query.get(URIHelper.DIGITS))\n                    .map(Integer::valueOf)\n                    .ifPresent(builder.hotpBuilder::withPasswordLength);\n            Optional.ofNullable(query.get(URIHelper.ALGORITHM))\n                    .map(String::toUpperCase)\n                    .map(HMACAlgorithm::valueOf)\n                    .ifPresent(builder.hotpBuilder::withAlgorithm);\n        } catch (Exception e) {\n            throw new URISyntaxException(uri.toString(), \"URI could not be parsed\");\n        }\n\n        return builder.build();\n    }\n\n    public static TOTPGenerator withDefaultValues(final byte[] secret) {\n        return new TOTPGenerator.Builder(secret).build();\n    }\n\n    public String now() throws IllegalStateException {\n        long counter = calculateCounter(clock, period);\n        return hotpGenerator.generate(counter);\n    }\n\n    public String now(Clock clock) throws IllegalStateException {\n        long counter = calculateCounter(clock, period);\n        return hotpGenerator.generate(counter);\n    }\n\n    public String at(final Instant instant) throws IllegalStateException {\n        return at(instant.getEpochSecond());\n    }\n\n    public String at(final Date date) throws IllegalStateException {\n        long secondsSince1970 = TimeUnit.MILLISECONDS.toSeconds(date.getTime());\n        return at(secondsSince1970);\n    }\n\n    public String at(final LocalDate date) throws IllegalStateException {\n        long secondsSince1970 = date.atStartOfDay(clock.getZone()).toEpochSecond();\n        return at(secondsSince1970);\n    }\n\n    public String at(final long secondsPast1970) throws IllegalArgumentException {\n        if (!validateTime(secondsPast1970))\n            throw new IllegalArgumentException(\"Time must be above zero\");\n\n        long counter = calculateCounter(secondsPast1970, period);\n        return hotpGenerator.generate(counter);\n    }\n\n    public boolean verify(final String code) {\n        long counter = calculateCounter(clock, period);\n        return hotpGenerator.verify(code, counter);\n    }\n\n    /**\n     * Checks whether a code is valid for a specific counter taking a delay window into account\n     *\n     * @param code an OTP code\n     * @param delayWindow window in which a code can still be deemed valid\n     * @return a boolean, true if code is valid, otherwise false\n     */\n    public boolean verify(final String code, final int delayWindow) {\n        long counter = calculateCounter(clock, period);\n        return hotpGenerator.verify(code, counter, delayWindow);\n    }\n\n    public URI getURI(final String issuer) throws URISyntaxException {\n        return getURI(issuer, \"\");\n    }\n\n    public URI getURI(final String issuer, final String account) throws URISyntaxException {\n        Map<String, String> query = new HashMap<>();\n        query.put(URIHelper.PERIOD, String.valueOf(period.getSeconds()));\n\n        return hotpGenerator.getURI(OTP_TYPE, issuer, account, query);\n    }\n\n    /**\n     * Calculates time until next time window will be reached and a new totp should be generated\n     *\n     * @return a duration object with duration until next time window\n     */\n    public Duration durationUntilNextTimeWindow() {\n        return durationUntilNextTimeWindow(clock);\n    }\n\n    public Duration durationUntilNextTimeWindow(Clock clock) {\n        long timeInterval = period.toMillis();\n        return Duration.ofMillis(timeInterval - clock.millis() % timeInterval);\n    }\n\n    public Duration getPeriod() {\n        return period;\n    }\n\n    public Clock getClock() {\n        return clock;\n    }\n\n    public HMACAlgorithm getAlgorithm() {\n        return hotpGenerator.getAlgorithm();\n    }\n\n    public int getPasswordLength() {\n        return hotpGenerator.getPasswordLength();\n    }\n\n    private long calculateCounter(final long secondsPast1970, final Duration period) {\n        return TimeUnit.SECONDS.toMillis(secondsPast1970) / period.toMillis();\n    }\n\n    private long calculateCounter(final Clock clock, final Duration period) {\n        return clock.millis() / period.toMillis();\n    }\n\n    private boolean validateTime(final long time) {\n        return time > 0;\n    }\n\n    public static final class Builder {\n\n        private Duration period;\n\n        private Clock clock;\n\n        private final HOTPGenerator.Builder hotpBuilder;\n\n        /**\n         * Creates a new builder.\n         * <p>\n         * Use {@link SecretGenerator#generate()} to create a secret.\n         * <p>\n         * If you are using a shared secret from another generator, you would likely need to encode it using\n         * {@link org.apache.commons.codec.binary.Base32#encode(byte[])}}\n         *\n         * @param secret Base32 encoded secret\n         */\n        public Builder(byte[] secret) {\n            this.period = DEFAULT_PERIOD;\n            this.clock = DEFAULT_CLOCK;\n            this.hotpBuilder = new HOTPGenerator.Builder(secret);\n        }\n\n        /**\n         * @param secret Base32 encoded secret\n         */\n        public Builder(String secret) {\n            this(secret.getBytes(UTF_8));\n        }\n\n        public Builder withHOTPGenerator(Consumer<HOTPGenerator.Builder> builder) {\n            builder.accept(hotpBuilder);\n            return this;\n        }\n\n        public Builder withClock(Clock clock) {\n            this.clock = clock;\n            return this;\n        }\n\n        public Builder withPeriod(Duration period) {\n            if (period.getSeconds() < 1) throw new IllegalArgumentException(\"Period must be at least 1 second\");\n            this.period = period;\n            return this;\n        }\n\n        public TOTPGenerator build() {\n            return new TOTPGenerator(this);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/bastiaanjansen/otp/helpers/URIHelper.java",
    "content": "package com.bastiaanjansen.otp.helpers;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n/**\n * A URI utility class with helper methods\n *\n * @author Bastiaan Jansen\n */\npublic class URIHelper {\n    \n    public static final String DIGITS = \"digits\";\n    public static final String SECRET = \"secret\";\n    public static final String ALGORITHM = \"algorithm\";\n    public static final String PERIOD = \"period\";\n    public static final String COUNTER = \"counter\";\n    public static final String ISSUER = \"issuer\";\n\n    private URIHelper() {}\n\n    /**\n     * Get a map of query items from URI\n     *\n     * @param uri to get query items from\n     * @return map of query items from URI\n     */\n    public static Map<String, String> queryItems(URI uri) {\n        Map<String, String> items = new LinkedHashMap<>();\n        String query = uri.getQuery();\n        String[] pairs = query.split(\"&\");\n\n        for (String pair: pairs) {\n            int index = pair.indexOf(\"=\");\n            try {\n                items.put(\n                        URLDecoder.decode(pair.substring(0, index), StandardCharsets.UTF_8.toString()),\n                        URLDecoder.decode(pair.substring(index + 1), StandardCharsets.UTF_8.toString())\n                );\n            } catch (UnsupportedEncodingException e) {\n                throw new IllegalStateException(\"Encoding should be supported\");\n            }\n        }\n        return items;\n    }\n\n    /**\n     * Create a URI based on a scheme, host, path and query items\n     *\n     * @param scheme of URI\n     * @param host of URI\n     * @param path of URI\n     * @param query of URI\n     * @return created URI\n     * @throws URISyntaxException when URI cannot be created\n     */\n    public static URI createURI(String scheme, String host, String path, Map<String, String> query) throws URISyntaxException {\n        String uriString = String.format(\"%s://%s/%s?\", scheme, host, path);\n\n        String uri = query.keySet().stream()\n                .map(key -> String.format(\"%s=%s\", key, encode(query.get(key))))\n                .collect(Collectors.joining(\"&\", uriString, \"\"));\n\n        return new URI(uri);\n    }\n\n    public static String encode(String value) {\n        try {\n            return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());\n        } catch (UnsupportedEncodingException e) {\n            throw new IllegalArgumentException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/bastiaanjansen/otp/ExampleApp.java",
    "content": "package com.bastiaanjansen.otp;\r\n\r\nimport java.time.Clock;\r\nimport java.time.Duration;\r\n\r\npublic class ExampleApp {\r\n    public static void main(String[] args) {\r\n\r\n        // Generate a secret, if you don't have one already\r\n        byte[] secret = SecretGenerator.generate();\r\n        Clock clock = Clock.systemUTC();\r\n\r\n        // Create a TOTPGenerate instance\r\n        TOTPGenerator totpGenerator = new TOTPGenerator.Builder(secret)\r\n                .withHOTPGenerator(builder -> {\r\n                    builder.withAlgorithm(HMACAlgorithm.SHA1);\r\n                    builder.withPasswordLength(6);\r\n                })\r\n                .withClock(clock)\r\n                .withPeriod(Duration.ofMinutes(15))\r\n                .build();\r\n\r\n        try {\r\n            String code = totpGenerator.now();\r\n            System.out.println(\"Generated code: \" + code);\r\n\r\n            // To verify a code\r\n            totpGenerator.verify(code); // true\r\n        } catch (IllegalStateException e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/test/java/com/bastiaanjansen/otp/HOTPGeneratorTest.java",
    "content": "package com.bastiaanjansen.otp;\n\nimport org.hamcrest.Matchers;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.junit.jupiter.params.provider.ValueSource;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.stream.Stream;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nclass HOTPGeneratorTest {\n\n    private final String secret = \"vv3kox7uqj4kyakohmzpph3us4cjimh6f3zknb5c2oobq6v2kiyhm27q\";\n\n    private static Stream<Arguments> testData() {\n        return Stream.of(\n                Arguments.of(6, 1, HMACAlgorithm.SHA1, \"560287\"),\n                Arguments.of(7, 1, HMACAlgorithm.SHA1, \"1560287\"),\n                Arguments.of(8, 1, HMACAlgorithm.SHA1, \"61560287\"),\n\n                Arguments.of(6, 1000, HMACAlgorithm.SHA1, \"401796\"),\n                Arguments.of(6, 923892, HMACAlgorithm.SHA1, \"793394\"),\n                Arguments.of(6, 82764924, HMACAlgorithm.SHA1, \"022826\"),\n\n                Arguments.of(6, 1, HMACAlgorithm.SHA256, \"361406\"),\n                Arguments.of(6, 1, HMACAlgorithm.SHA512, \"016738\"),\n                Arguments.of(6, 1, HMACAlgorithm.SHA224, \"422784\"),\n                Arguments.of(6, 1, HMACAlgorithm.SHA384, \"466320\")\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"testData\")\n    void generateWithCounter(int passwordLength, long counter, HMACAlgorithm algorithm, String otp) {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret)\n                .withPasswordLength(passwordLength)\n                .withAlgorithm(algorithm)\n                .build();\n\n        assertThat(generator.generate(counter), is(otp));\n    }\n\n    @ParameterizedTest\n    @ValueSource(ints = {-1, -100})\n    void generateWithInvalidCounter_throwsIllegalArgumentException(long counter) {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        assertThrows(IllegalArgumentException.class, () -> generator.generate(counter));\n    }\n\n    @Test\n    void verifyCurrentCode_true() {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n        String code = generator.generate(1);\n\n        assertThat(generator.verify(code, 1), is(true));\n    }\n\n    @Test\n    void verifyOlderCodeWithDelayWindowIs0_false() {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n        String code = generator.generate(1);\n\n        assertThat(generator.verify(code, 2), is(false));\n    }\n\n    @Test\n    void verifyOlderCodeWithDelayWindowIs1_true() {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n        String code = generator.generate(1);\n\n        assertThat(generator.verify(code, 2, 1), is(true));\n    }\n\n    @Test\n    void withDefaultValues_algorithm() {\n        HOTPGenerator generator = HOTPGenerator.withDefaultValues(secret.getBytes());\n        HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n        assertThat(generator.getAlgorithm(), is(expected));\n    }\n\n    @Test\n    void withDefaultValues_passwordLength() {\n        HOTPGenerator generator = HOTPGenerator.withDefaultValues(secret.getBytes());\n        int expected = 6;\n\n        assertThat(generator.getPasswordLength(), is(expected));\n    }\n\n    @Test\n    void getURIWithIssuer_doesNotThrow() {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        assertDoesNotThrow(() -> {\n           generator.getURI(10, \"issuer\");\n        });\n    }\n\n    @Test\n    void getURIWithIssuerWithSpace_doesNotThrow() {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        assertDoesNotThrow(() -> generator.getURI(10, \"issuer with space\"));\n    }\n\n    @Test\n    void getURIWithIssuerWithSpace_doesEscapeIssuer() throws URISyntaxException {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        String url = generator.getURI(10, \"issuer with space\").toString();\n\n        assertThat(url, is(\"otpauth://hotp/issuer+with+space?digits=6&counter=10&secret=vv3kox7uqj4kyakohmzpph3us4cjimh6f3zknb5c2oobq6v2kiyhm27q&issuer=issuer+with+space&algorithm=SHA1\"));\n    }\n\n    @Test\n    void getURIWithIssuer() throws URISyntaxException {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n        URI uri = generator.getURI(10, \"issuer\");\n\n        assertThat(uri.toString(), is(\"otpauth://hotp/issuer?digits=6&counter=10&secret=\" + secret + \"&issuer=issuer&algorithm=SHA1\"));\n    }\n\n    @Test\n    void getURIWithIssuerWithUrlUnsafeCharacters() throws URISyntaxException {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n        URI uri = generator.getURI(10, \"mac&cheese\");\n\n        assertThat(uri.toString(), is(\"otpauth://hotp/mac%26cheese?digits=6&counter=10&secret=\" + secret + \"&issuer=mac%26cheese&algorithm=SHA1\"));\n    }\n\n\n    @Test\n    void getURIWithIssuerAndAccount_doesNotThrow() {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        assertDoesNotThrow(() -> {\n            generator.getURI(100, \"issuer\", \"account\");\n        });\n    }\n\n    @Test\n    void getURIWithIssuerAndAccount() throws URISyntaxException {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        URI uri = generator.getURI(100, \"issuer\", \"account\");\n        assertThat(uri.toString(), is(\"otpauth://hotp/issuer:account?digits=6&counter=100&secret=\" + secret + \"&issuer=issuer&algorithm=SHA1\"));\n    }\n\n    @Test\n    void getURIWithIssuerAndAccountWithUrlUnsafeCharacters() throws URISyntaxException {\n        HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n\n        URI uri = generator.getURI(100, \"mac&cheese\", \"ac@cou.nt\");\n\n        assertThat(uri.toString(), is(\"otpauth://hotp/mac%26cheese:ac%40cou.nt?digits=6&counter=100&secret=\" + secret + \"&issuer=mac%26cheese&algorithm=SHA1\"));\n    }\n\n    @Test\n    void fromURIWithAlgorithmUppercase() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://hotp/issuer?counter=10&algorithm=SHA256&secret=\" + secret);\n\n        HOTPGenerator generator = HOTPGenerator.fromURI(uri);\n        HMACAlgorithm expected = HMACAlgorithm.SHA256;\n\n        assertThat(generator.getAlgorithm(), is(expected));\n    }\n\n    @Test\n    void fromURIWithAlgorithmLowercase() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://hotp/issuer?counter=10&algorithm=sha256&secret=\" + secret);\n        HOTPGenerator generator = HOTPGenerator.fromURI(uri);\n        HMACAlgorithm expected = HMACAlgorithm.SHA256;\n\n        assertThat(generator.getAlgorithm(), is(expected));\n    }\n\n    @Test\n    void fromURIWithDigitsIs7() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://hotp/issuer?digits=7&counter=10&secret=\" + secret);\n        HOTPGenerator generator = HOTPGenerator.fromURI(uri);\n        int expected = 7;\n\n        assertThat(generator.getPasswordLength(), is(expected));\n    }\n\n    @Test\n    void fromURIWithInvalidDigits_throwsURISyntaxException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://hotp/issuer?digits=invalid&counter=10&secret=\" + secret);\n\n        assertThrows(URISyntaxException.class, () -> HOTPGenerator.fromURI(uri));\n    }\n\n    @Test\n    void fromURIWithInvalidAlgorithm_throwsURISyntaxException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://hotp/issuer?counter=10&algorithm=invalid&secret=\" + secret);\n\n        assertThrows(URISyntaxException.class, () -> HOTPGenerator.fromURI(uri));\n    }\n\n    @Test\n    void fromURIWithInvalidSecret_throwsIllegalArgumentException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://hotp/issuer?counter=10\");\n\n        assertThrows(IllegalArgumentException.class, () -> HOTPGenerator.fromURI(uri));\n    }\n\n    @Nested\n    class BuilderTest {\n        @Test\n        void builderWithEmptySecret_throwsIllegalArgumentException() {\n            assertThrows(IllegalArgumentException.class, () -> new HOTPGenerator.Builder(new byte[]{}).build());\n        }\n\n        @Test\n        void builderWithPasswordLengthIs5_throwsIllegalArgumentException() {\n            assertThrows(IllegalArgumentException.class, () -> {\n                new HOTPGenerator.Builder(secret).withPasswordLength(5).build();\n            });\n        }\n\n        @Test\n        void builderWithPasswordLengthIs9_throwsIllegalArgumentException() {\n            assertThrows(IllegalArgumentException.class, () -> {\n                new HOTPGenerator.Builder(secret).withPasswordLength(9).build();\n            });\n        }\n\n        @Test\n        void builderWithPasswordLengthIs6() {\n            HOTPGenerator generator = new HOTPGenerator.Builder(secret).withPasswordLength(6).build();\n            int expected = 6;\n\n            assertThat(generator.getPasswordLength(), Matchers.is(expected));\n        }\n\n        @Test\n        void builderWithAlgorithmSHA1() {\n            HOTPGenerator generator = new HOTPGenerator.Builder(secret).withAlgorithm(HMACAlgorithm.SHA1).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n            assertThat(generator.getAlgorithm(), Matchers.is(expected));\n        }\n\n        @Test\n        void builderWithAlgorithmSHA256() {\n            HOTPGenerator generator = new HOTPGenerator.Builder(secret).withAlgorithm(HMACAlgorithm.SHA256).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA256;\n\n            assertThat(generator.getAlgorithm(), Matchers.is(expected));\n        }\n\n        @Test\n        void builderWithAlgorithmSHA512() {\n            HOTPGenerator generator = new HOTPGenerator.Builder(secret).withAlgorithm(HMACAlgorithm.SHA512).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA512;\n\n            assertThat(generator.getAlgorithm(), Matchers.is(expected));\n        }\n\n        @ParameterizedTest\n        @ValueSource(ints = { 1, 2, 3, 4, 5, 9, 10 })\n        void builderWithInvalidPasswordLength_throwsIllegalArgumentException(int passwordLength) {\n            assertThrows(IllegalArgumentException.class, () -> new HOTPGenerator.Builder(secret).withPasswordLength(passwordLength).build());\n        }\n\n        @Test\n        void builderWithoutAlgorithm_defaultAlgorithm() {\n            HOTPGenerator generator = new HOTPGenerator.Builder(secret).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n            assertThat(generator.getAlgorithm(), is(expected));\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/bastiaanjansen/otp/SecretGeneratorTest.java",
    "content": "package com.bastiaanjansen.otp;\r\n\r\nimport org.junit.jupiter.api.Test;\r\n\r\nimport static org.hamcrest.CoreMatchers.is;\r\nimport static org.hamcrest.MatcherAssert.assertThat;\r\nimport static org.junit.jupiter.api.Assertions.assertThrows;\r\n\r\nclass SecretGeneratorTest {\r\n\r\n    @Test\r\n    void generate_defaultLengthIs32() {\r\n        int expected = 32;\r\n        assertThat(SecretGenerator.generate().length, is(expected));\r\n    }\r\n\r\n    @Test\r\n    void generate_lengthIs56() {\r\n        int expected = 56;\r\n        assertThat(SecretGenerator.generate(256).length, is(expected));\r\n    }\r\n\r\n    @Test\r\n    void generateWithZeroBits_throwsIllegalArgument() {\r\n        assertThrows(IllegalArgumentException.class, () -> SecretGenerator.generate(0));\r\n    }\r\n\r\n    @Test\r\n    void generateWithLessThanZeroBits_throwsIllegalArgument() {\r\n        assertThrows(IllegalArgumentException.class, () -> SecretGenerator.generate(-1));\r\n    }\r\n}"
  },
  {
    "path": "src/test/java/com/bastiaanjansen/otp/TOTPGeneratorTest.java",
    "content": "package com.bastiaanjansen.otp;\n\nimport org.hamcrest.Matchers;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.junit.jupiter.params.provider.ValueSource;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.util.Date;\nimport java.util.stream.Stream;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nclass TOTPGeneratorTest {\n\n    private final static String secret = \"vv3kox7uqj4kyakohmzpph3us4cjimh6f3zknb5c2oobq6v2kiyhm27q\";\n\n    private static Stream<Arguments> secondsPast1970TestData() {\n        return Stream.of(\n                Arguments.of(6, 1, HMACAlgorithm.SHA1, \"455216\"),\n                Arguments.of(7, 1, HMACAlgorithm.SHA1, \"7455216\"),\n                Arguments.of(8, 1, HMACAlgorithm.SHA1, \"17455216\"),\n\n                Arguments.of(6, 1000, HMACAlgorithm.SHA1, \"687469\"),\n                Arguments.of(6, 923892, HMACAlgorithm.SHA1, \"909546\"),\n                Arguments.of(6, 82764924, HMACAlgorithm.SHA1, \"408978\"),\n\n                Arguments.of(6, 1, HMACAlgorithm.SHA256, \"755370\"),\n                Arguments.of(6, 1, HMACAlgorithm.SHA512, \"303161\")\n        );\n    }\n\n    private static Stream<Arguments> instantTestData() {\n        return Stream.of(\n                Arguments.of(6, Instant.ofEpochSecond(1), HMACAlgorithm.SHA1, \"455216\"),\n                Arguments.of(7, Instant.ofEpochSecond(1000), HMACAlgorithm.SHA1, \"0687469\"),\n                Arguments.of(8, Instant.ofEpochSecond(923892), HMACAlgorithm.SHA1, \"39909546\"),\n                Arguments.of(6, Instant.ofEpochSecond(82764924), HMACAlgorithm.SHA256, \"999993\"),\n                Arguments.of(6, Instant.ofEpochSecond(82764924), HMACAlgorithm.SHA512, \"300089\")\n        );\n    }\n\n    private static Stream<Arguments> dateTestData() {\n        return Stream.of(\n                Arguments.of(6, Date.from(Instant.ofEpochSecond(1)), HMACAlgorithm.SHA1, \"455216\"),\n                Arguments.of(7, Date.from(Instant.ofEpochSecond(100)), HMACAlgorithm.SHA1, \"9650012\"),\n                Arguments.of(8, Date.from(Instant.ofEpochSecond(723)), HMACAlgorithm.SHA1, \"12251322\"),\n                Arguments.of(6, Date.from(Instant.ofEpochSecond(123)), HMACAlgorithm.SHA256, \"376047\"),\n                Arguments.of(6, Date.from(Instant.ofEpochSecond(9802467)), HMACAlgorithm.SHA512, \"040816\")\n        );\n    }\n\n    private static Stream<Arguments> clockTestData() {\n        return Stream.of(\n                Arguments.of(6, Clock.fixed(Instant.ofEpochSecond(1), ZoneId.of(\"UTC\")), HMACAlgorithm.SHA1, \"455216\"),\n                Arguments.of(7, Clock.fixed(Instant.ofEpochSecond(100), ZoneId.of(\"UTC\")), HMACAlgorithm.SHA1, \"9650012\"),\n                Arguments.of(8, Clock.fixed(Instant.ofEpochSecond(723), ZoneId.of(\"UTC\")), HMACAlgorithm.SHA1, \"12251322\"),\n                Arguments.of(6, Clock.fixed(Instant.ofEpochSecond(123), ZoneId.of(\"UTC\")), HMACAlgorithm.SHA256, \"376047\"),\n                Arguments.of(6, Clock.fixed(Instant.ofEpochSecond(9802467), ZoneId.of(\"UTC\")), HMACAlgorithm.SHA512, \"040816\")\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"secondsPast1970TestData\")\n    void generateAtSecondsPast1970(int passwordLength, int secondsPast1970, HMACAlgorithm algorithm, String otp) {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {\n            builder.withPasswordLength(passwordLength);\n            builder.withAlgorithm(algorithm);\n        }).build();\n\n        assertThat(generator.at(secondsPast1970), is(otp));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"instantTestData\")\n    void generateAtInstant(int passwordLength, Instant instant, HMACAlgorithm algorithm, String otp) {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {\n            builder.withPasswordLength(passwordLength);\n            builder.withAlgorithm(algorithm);\n        }).build();\n\n        assertThat(generator.at(instant), is(otp));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"dateTestData\")\n    void generateAtDate(int passwordLength, Date date, HMACAlgorithm algorithm, String otp) {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {\n            builder.withPasswordLength(passwordLength);\n            builder.withAlgorithm(algorithm);\n        }).build();\n\n        assertThat(generator.at(date), is(otp));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"clockTestData\")\n    void generateAtNow(int passwordLength, Clock clock, HMACAlgorithm algorithm, String otp) {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> {\n                    builder.withPasswordLength(passwordLength);\n                    builder.withAlgorithm(algorithm);\n                })\n                .withClock(clock)\n                .build();\n\n        assertThat(generator.now(), is(otp));\n    }\n\n\n    @ParameterizedTest\n    @ValueSource(ints = {0, -1})\n    void generateWithInvalidSecondsPast1970_throwsIllegalArgumentException(int secondsPast1970) {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n\n        assertThrows(IllegalArgumentException.class, () -> generator.at(secondsPast1970));\n    }\n\n\n    @Test\n    void verifyCurrentCode_true() {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n        String code = generator.now();\n\n        assertThat(generator.verify(code), is(true));\n    }\n\n    @Test\n    void verifyOlderCodeWithDelayWindowIs0_false() {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n        String code = generator.at(Instant.now().minusSeconds(30));\n\n        assertThat(generator.verify(code), is(false));\n    }\n\n    @Test\n    void verifyOlderCodeWithDelayWindowIs1_true() {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n        String code = generator.at(Instant.now().minusSeconds(30));\n\n        assertThat(generator.verify(code, 1), is(true));\n    }\n\n\n    @Test\n    void getURIWithIssuer() throws URISyntaxException {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n\n        URI uri = generator.getURI(\"issuer\");\n        assertThat(uri.toString(), is(\"otpauth://totp/issuer?period=30&digits=6&secret=\" + secret + \"&issuer=issuer&algorithm=SHA1\"));\n    }\n\n    @Test\n    void getURIWithIssuerWithUrlUnsafeCharacters() throws URISyntaxException {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n\n        URI uri = generator.getURI(\"mac&cheese\");\n        assertThat(uri.toString(), is(\"otpauth://totp/mac%26cheese?period=30&digits=6&secret=\" + secret + \"&issuer=mac%26cheese&algorithm=SHA1\"));\n    }\n\n    @Test\n    void getURIWithIssuerAndAccount_doesNotThrow() {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n\n        assertDoesNotThrow(() -> {\n            generator.getURI(\"issuer\", \"account\");\n        });\n    }\n\n    @Test\n    void getURIWithIssuerAndAccount() throws URISyntaxException {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n\n\n        URI uri = generator.getURI(\"issuer\", \"account\");\n        assertThat(uri.toString(), is(\"otpauth://totp/issuer:account?period=30&digits=6&secret=\" + secret + \"&issuer=issuer&algorithm=SHA1\"));\n    }\n\n    @Test\n    void getURIWithIssuerAndAccountWithUrlUnsafeCharacters() throws URISyntaxException {\n        TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n\n\n        URI uri = generator.getURI(\"mac&cheese\", \"ac@cou.nt\");\n        assertThat(uri.toString(), is(\"otpauth://totp/mac%26cheese:ac%40cou.nt?period=30&digits=6&secret=\" + secret + \"&issuer=mac%26cheese&algorithm=SHA1\"));\n    }\n\n    @Test\n    void fromURIWithPeriod() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?period=60&secret=\" + secret);\n\n        TOTPGenerator generator = TOTPGenerator.fromURI(uri);\n        Duration expected = Duration.ofSeconds(60);\n\n        assertThat(generator.getPeriod(), is(expected));\n    }\n\n    @Test\n    void fromURIWithAlgorithmUppercase() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?algorithm=SHA1&secret=\" + secret);\n\n        TOTPGenerator generator = TOTPGenerator.fromURI(uri);\n        HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n        assertThat(generator.getAlgorithm(), is(expected));\n    }\n\n    @Test\n    void fromURIWithAlgorithmLowercase() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?algorithm=sha1&secret=\" + secret);\n\n        TOTPGenerator generator = TOTPGenerator.fromURI(uri);\n        HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n        assertThat(generator.getAlgorithm(), is(expected));\n    }\n\n    @Test\n    void fromURIWithPasswordLength() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?digits=6&secret=\" + secret);\n\n        TOTPGenerator generator = TOTPGenerator.fromURI(uri);\n        int expected = 6;\n\n        assertThat(generator.getPasswordLength(), is(expected));\n    }\n\n    @Test\n    void fromURIWithInvalidPeriod_throwsURISyntaxException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?period=invalid&secret=\" + secret);\n\n        assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));\n    }\n\n    @Test\n    void fromURIWithPasswordLengthIs5_throwsURISyntaxException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?digits=5&secret=\" + secret);\n\n        assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));\n    }\n\n    @Test\n    void fromURIWithPasswordLengthIs9_throwsURISyntaxException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?digits=9&secret=\" + secret);\n\n        assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\n            \"otpauth://totp/issuer:account?digits=6&secret=\",\n            \"otpauth://totp/issuer:account?digits=8&secret=\",\n            \"otpauth://totp/issuer:account?digits=7&secret=\"\n    })\n    void fromURI_doesNotThrow(String url) throws URISyntaxException {\n        URI uri = new URI(url + secret);\n\n        assertDoesNotThrow(() -> {\n            TOTPGenerator.fromURI(uri);\n        });\n    }\n\n    @Test\n    void fromURIWithInvalidAlgorithm_throwsURISyntaxException() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?algorithm=invalid&secret=\" + secret);\n\n        assertThrows(URISyntaxException.class, () -> TOTPGenerator.fromURI(uri));\n    }\n\n    @Nested\n    class BuilderTest {\n        @Test\n        void builderWithEmptySecret_throwsIllegalArgumentException() {\n            assertThrows(IllegalArgumentException.class, () -> new TOTPGenerator.Builder(new byte[]{}).build());\n        }\n\n        @Test\n        void builderWithPasswordLengthIs5_throwsIllegalArgumentException() {\n            assertThrows(IllegalArgumentException.class, () -> {\n                new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(5)).build();\n            });\n        }\n\n        @Test\n        void builderWithPasswordLengthIs9_throwsIllegalArgumentException() {\n            assertThrows(IllegalArgumentException.class, () -> {\n                new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(9)).build();\n            });\n        }\n\n        @Test\n        void builderWithPasswordLengthIs6() {\n            TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(6)).build();\n            int expected = 6;\n\n            assertThat(generator.getPasswordLength(), Matchers.is(expected));\n        }\n\n        @Test\n        void builderWithAlgorithmSHA1() {\n            TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withAlgorithm(HMACAlgorithm.SHA1)).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n            assertThat(generator.getAlgorithm(), Matchers.is(expected));\n        }\n\n        @Test\n        void builderWithAlgorithmSHA256() {\n            TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withAlgorithm(HMACAlgorithm.SHA256)).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA256;\n\n            assertThat(generator.getAlgorithm(), Matchers.is(expected));\n        }\n\n        @Test\n        void builderWithAlgorithmSHA512() {\n            TOTPGenerator generator = new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withAlgorithm(HMACAlgorithm.SHA512)).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA512;\n\n            assertThat(generator.getAlgorithm(), Matchers.is(expected));\n        }\n\n        @ParameterizedTest\n        @ValueSource(ints = {1, 2, 3, 4, 5, 9, 10})\n        void builderWithInvalidPasswordLength_throwsIllegalArgumentException(int passwordLength) {\n            assertThrows(IllegalArgumentException.class, () -> new TOTPGenerator.Builder(secret).withHOTPGenerator(builder -> builder.withPasswordLength(passwordLength)).build());\n        }\n\n        @Test\n        void builderWithoutPeriod_defaultPeriod() {\n            TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n            Duration expected = Duration.ofSeconds(30);\n\n            assertThat(generator.getPeriod(), is(expected));\n        }\n\n        @Test\n        void builderWithoutAlgorithm_defaultAlgorithm() {\n            TOTPGenerator generator = new TOTPGenerator.Builder(secret).build();\n            HMACAlgorithm expected = HMACAlgorithm.SHA1;\n\n            assertThat(generator.getAlgorithm(), is(expected));\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/bastiaanjansen/otp/helpers/URIHelperTest.java",
    "content": "package com.bastiaanjansen.otp.helpers;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\n\nclass URIHelperTest {\n\n    @Test\n    void queryItemsWithOneQueryItem() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?algorithm=SHA1\");\n        Map<String, String> query = URIHelper.queryItems(uri);\n        String expected = \"SHA1\";\n\n        assertThat(query.get(\"algorithm\"), is(expected));\n    }\n\n    @Test\n    void queryItemsWithTwoQueryItems() throws URISyntaxException {\n        URI uri = new URI(\"otpauth://totp/issuer:account?algorithm=SHA1&secret=ABC\");\n        Map<String, String> query = URIHelper.queryItems(uri);\n        String expected = \"ABC\";\n\n        assertThat(query.get(\"secret\"), is(expected));\n    }\n\n    @Test\n    void createURI_doesNotThrow() {\n        Map<String, String> query = new HashMap<>();\n\n        assertDoesNotThrow(() -> URIHelper.createURI(\"scheme\", \"host\", \"path\", query));\n    }\n\n    @Test\n    void createURIWithOneQueryItem() throws URISyntaxException {\n        Map<String, String> query = new HashMap<>();\n        query.put(\"test\", \"1\");\n        URI uri = URIHelper.createURI(\"scheme\", \"host\", \"path\", query);\n        String expected = \"scheme://host/path?test=1\";\n\n        assertThat(uri.toString(), is(expected));\n    }\n\n    @Test\n    void createURIWithTwoQueryItems() throws URISyntaxException {\n        Map<String, String> query = new HashMap<>();\n        query.put(\"test\", \"1\");\n        query.put(\"test2\", \"2\");\n        URI uri = URIHelper.createURI(\"scheme\", \"host\", \"path\", query);\n        String expected = \"scheme://host/path?test2=2&test=1\";\n\n        assertThat(uri.toString(), is(expected));\n    }\n\n    @Test\n    void createURIWithUrlUnsafeCharacters() throws URISyntaxException {\n        Map<String, String> query = new HashMap<>();\n        query.put(\"test\", \"value 1\");\n        query.put(\"test2\", \"value?&2\");\n        URI uri = URIHelper.createURI(\"scheme\", \"host\", \"path\", query);\n        String expected = \"scheme://host/path?test2=value%3F%262&test=value+1\";\n\n        assertThat(uri.toString(), is(expected));\n    }\n}\n"
  }
]