Git Product home page Git Product logo

java-totp's Introduction

Time-based One Time Password (MFA) Library for Java

CircleCI Coverage Status Maven Central

A java library to help generate and verify time-based one time passwords for Multi-Factor Authentication.

Generates QR codes that are recognisable by applications like Google Authenticator, and verify the one time passwords they produce.

Inspired by PHP library for Two Factor Authentication, a similar library for PHP.

Requirements

  • Java 8+

Spring Boot

The quickest way to start using this library in a Spring Boot project is to require the TOTP Spring Boot Starter. See Using Java-TOTP with Spring Boot for more information, or read on to learn about the library.

Installation

Maven

To add this library to your java project using Maven, add the following dependency:

<dependency>
  <groupId>dev.samstevens.totp</groupId>
  <artifactId>totp</artifactId>
  <version>1.7.1</version>
</dependency>

Gradle

To add the dependency using Gradle, add the following to the build script:

dependencies {
  compile 'dev.samstevens.totp:totp:1.7.1'
}

Usage

Generating a shared secret

To generate a secret, use the dev.samstevens.totp.secret.DefaultSecretGenerator class.

SecretGenerator secretGenerator = new DefaultSecretGenerator();
String secret = secretGenerator.generate();
// secret = "BP26TDZUZ5SVPZJRIHCAUVREO5EWMHHV"

By default, this class generates secrets that are 32 characters long, but this number is configurable via

the class constructor.

// Generates secrets that are 64 characters long
SecretGenerator secretGenerator = new DefaultSecretGenerator(64);

Generating a QR code

Once a shared secret has been generated, this must be given to the user so they can add it to an MFA application, such as Google Authenticator. Whilst they could just enter the secret manually, a much better and more common option is to generate a QR code containing the secret (and other information), which can then be scanned by the application.

To generate such a QR code, first create a dev.samstevens.totp.qr.QrData instance with the relevant information.

 QrData data = new QrData.Builder()
   .label("[email protected]")
   .secret(secret)
   .issuer("AppName")
   .algorithm(HashingAlgorithm.SHA1) // More on this below
   .digits(6)
   .period(30)
   .build();

Once you have a QrData object holding the relevant details, a PNG image of the code can be generated using the dev.samstevens.totp.qr.ZxingPngQrGenerator class.

QrGenerator generator = new ZxingPngQrGenerator();
byte[] imageData = generator.generate(data)

The generate method returns a byte array of the raw image data. The mime type of the data that is generated by the generator can be retrieved using the getImageMimeType method.

String mimeType = generator.getImageMimeType();
// mimeType = "image/png"

The image data can then be outputted to the browser, or saved to a temporary file to show it to the user.

Embedding the QR code within HTML

To avoid the QR code image having to be saved to disk, or passing the shared secret to another endpoint that generates and returns the image, it can be encoded in a Data URI, and embedded directly in the HTML served to the user.

import static dev.samstevens.totp.util.Utils.getDataUriForImage;
...
String dataUri = getDataUriForImage(imageData, mimeType);
// dataUri = data:image/png;base64,iVBORw0KGgoAAAANSU...

The QR code image can now be embedded directly in HTML via the data URI. Below is an example using Thymeleaf:

<img th:src="${dataUri}" />

Verifying one time passwords

After a user sets up their MFA, it's a good idea to get them to enter two of the codes generated by their app to verify the setup was successful. To verify a code submitted by the user, do the following:

TimeProvider timeProvider = new SystemTimeProvider();
CodeGenerator codeGenerator = new DefaultCodeGenerator();
CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);

// secret = the shared secret for the user
// code = the code submitted by the user
boolean successful = verifier.isValidCode(secret, code)

This same process is used when verifying the submitted code every time the user needs to in the future.

Using different hashing algorithms

By default, the DefaultCodeGenerator uses the SHA1 algorithm to generate/verify codes, but SHA256 and SHA512 are also supported. To use a different algorithm, pass in the desired HashingAlgorithm into the constructor:

CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA512);

When verifying a given code, you must use the same hashing algorithm that was specified when the QR code was generated for the secret, otherwise the user submitted codes will not match.

Setting the time period and discrepancy

The one time password codes generated in the authenticator apps only last for a certain time period before they are re-generated, and most implementations of TOTP allow room for codes that have recently expired, or will only "become valid" soon in the future to be accepted as valid, to allow for a small time drift between the server and the authenticator app (discrepancy).

By default on a DefaultCodeVerifier the time period is set to the standard 30 seconds, and the discrepancy to 1, to allow a time drift of +/-30 seconds. These values can be changed by calling the appropriate setters:

DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
// sets the time period for codes to be valid for to 60 seconds
verifier.setTimePeriod(60);

// allow codes valid for 2 time periods before/after to pass as valid
verifier.setAllowedTimePeriodDiscrepancy(2);

Like the hashing algorithm, the time period must be the same as the one specified when the QR code for the secret was created.

Setting how many digits long the generated codes are

Most TOTP implementations generate codes that are 6 digits long, but codes can have a length of any positive non-zero integer. The default number of digits in a code generated by a DefaultCodeGenerator instance is 6, but can be set to a different value by passing the number as the second parameter in the constructor:

CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA1, 4);

The above generator will generate codes of 4 digits, using the SHA1 algorithm.

Once again, the number of digits must be the same as what was specified when the QR code for the secret was created.

Using different time providers

When verifying user submitted codes with a DefaultCodeVerifier, a TimeProvider is needed to get the current time (unix) time. In the example code above, a SystemTimeProvider is used, but this is not the only option.

Getting the time from the system

Most applications should be able to use the SystemTimeProvider class to provide the time, which gets the time from the system clock. If the system clock is reliable, it is reccomended that this provider is used.

Getting the time from an NTP Server

If the system clock cannot be used to accurately get the current time, then you can fetch it from an NTP server with the dev.samstevens.totp.time.NtpTimeProvider class, passing in the NTP server hostname you wish you use.

TimeProvider timeProvider = new NtpTimeProvider("pool.ntp.org");

The default timeout for the requests to the NTP server is 3 seconds, but this can be set by passing in the desired number of milliseconds as the second parameter in the constructor:

TimeProvider timeProvider = new NtpTimeProvider("pool.ntp.org", 5000);

Using this time provider requires that the Apache Commons Net library is available on the classpath. Add the dependency to your project with Maven/Gradle like this:

Maven:

<dependency>
  <groupId>commons-net</groupId>
  <artifactId>commons-net</artifactId>
  <version>3.6</version>
</dependency>

Gradle:

dependencies {
  compile 'commons-net:commons-net:3.6'
}

Recovery Codes

Recovery codes can be used to allow users to gain access to their MFA protected account without providing a TOTP, bypassing the MFA process. This is usually given as an option to the user so that in the event of losing access to the device which they have registered the MFA secret with, they are still able to log in.

Usually, upon registering an account for MFA, several one-time use codes will be generated and presented to the user, with instructions to keep them very safe. When the user is presented with the prompt for a TOTP in the future, they can opt to enter one of the recovery codes instead to gain access to their account.

Most of the logic needed for implementing recovery codes (storage, associating them with a user, checking for existance, etc) is implementation specific, but the codes themselves can be generated via this library.

The default implementation provided in this library generates recovery codes :

  • of 16 characters
  • composed of numbers and lower case characters from latin alphabet (for a total of 36 possible characters)
  • split in groups separated with dash for better readability

Thoses settings guarantees recovery codes security (with an entropy of 82 bits) while keeping codes simple to read and enter by end user when needed.

import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
...
// Generate 16 random recovery codes
RecoveryCodeGenerator recoveryCodes = new RecoveryCodeGenerator();
String[] codes = recoveryCodes.generateCodes(16);
// codes = ["tf8i-exmo-3lcb-slkm", "boyv-yq75-z99k-r308", "w045-mq6w-mg1i-q12o", ...]

Running Tests

To run the tests for the library with Maven, run mvn test.

License

This project is licensed under the MIT license.

java-totp's People

Contributors

olivierjaquemet avatar paulo-raca avatar ralscha avatar samdjstevens avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

java-totp's Issues

RFE: Validate method for additional security

For a secure TOTP I need a method to check the validity of 3 TOTP codes.
In my example I check 24 hours ahead and behind. The 3 codes must exist in this period
and they must be consecutive.

The method must accept the secret and 3 TOTP codes and returns two results.

  • boolean valid: true = the 3 codes are valid and they are consecutive.
  • int shift: how many periods the codes are behind or ahead

Here an idea how this could look like:

    TimeProvider timeProvider = new SystemTimeProvider();
    CodeGenerator codeGenerator = new DefaultCodeGenerator();
    DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
    long periods = TimeUnit.HOURS.toSeconds(24) / 30; 
    //would be nice if I could use verifier.getTimePeriod(), instead of the hardcoded 30
    //or there is a method to set the discrepancy in seconds and the library calculates the periods
    
    verifier.setAllowedTimePeriodDiscrepancy((int)periods); 
    // setAllowedTimePeriodDiscrepancy does not accept long
  
    VerificationResult result = verifier.areCodesValid(secret, code1, code2, code3);
    //result should contain a boolean, if the codes are valid and consecutive
    //and it should return a time shift number (int). How many periods the codes are behind or ahead

For my example TOTP application I had to write my own TOTP verifier based on the now archived aerogear project. Would be nice if I could switch to your library.

See also this issue for more details why this method is needed.

RFE : declare commons-net as an optionnal dependency

In the current state of java-totp, the commons-net is only used for NtpTimeProvider feature.

My suggestion would be to declare commons-net as an optional dependency, and update the documentation, to indicate that use of NtpTimeProvider requires an additional explicit declaration of commons-net.
This would reduce the default transitive dependencies to the minimum required.

Resources :
https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html

Workaround for user of you library that wish to reduce their transitive dependencies : exclude commons-net from their pom :

     <dependency>
      <groupId>dev.samstevens.totp</groupId>
      <artifactId>totp</artifactId>
      <exclusions>
        <!-- Exclude commons-net, it is used by samsstevens-topt for Ntp implementation, we don't use this -->
        <exclusion>
          <groupId>commons-net</groupId>
          <artifactId>commons-net</artifactId>
        </exclusion>
        <!-- Exclude jcommander, it is used by xzing in commandline, we don't use this -->
        <exclusion>
          <groupId>com.beust</groupId>
          <artifactId>jcommander</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

PS : thanks for providing this nice and simple library. ๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿป

DataUriEncodingTest.testDataUriEncode fails on Java 11

When running mvn test on Java 11 the following test will fail :

[INFO] Running dev.samstevens.totp.util.DataUriEncodingTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.032 s <<< FAILURE! - in dev.samstevens.totp.util.DataUriEncodingTest
[ERROR] testDataUriEncode(dev.samstevens.totp.util.DataUriEncodingTest)  Time elapsed: 0.03 s  <<< FAILURE!
org.junit.ComparisonFailure: expected:<...AAAFeAQAAAADlUEq3AAA[C30lEQVR42u2bPZLiQAyFRREQ7hHmKBzNPpqPwhEcElDWtt5T2267ganaZIPnYIaxPwiEfp96zH9/vUywYMGCBf8jvBgu96f9TINdn38eNlxeN3/cx/IXHl4Fd+F45eNiN/fJQNlQHpndx6W8tSKCO/Az7vsr4GGpli1W97EYv1jdBH+El3wc78GjcNjlKvg77PTNi/scQW4WQW6CP8OMbtr5Ak9FmrS7v00Fgnc1Bd7IVNj86hYgwW0nA5OGi65v/dD2CK52Lk1LucJFy51HxDoSYwn5iPWL4D5Mk0YqLEGOOhyPeRNJ0wX3YeRAXOGihlQ4wsBOc7f+LNhbO6Py1o65VuVoAr3jooK3clxuxKvZwlM5rtHqoKJUC+7D7FY4tdWMyPzIcjwI7sPlJzKi3WBnpkkYPw082S4VCG7gLLk54hYDY9iYI9bjE2wX3YKt6WRufIzEiCaw/Hrl1HZqAgW35RiqXnjjwE7GjJ+AuffYyQje2XmmJhWygK3xHL4ZzzJNCj7DsDM1KbNqbmoFFBDyOxDcg6lCPbfSwhluGrKmCO7DcaOuLqyOuI7EaBze2o5RcE+hop0d+gomXWov42l9JnjrZNgLpjrAeJ4humCG2/fPgo8uaquyzJnD1gnkpD8LbkaPKaJ7TqfM3oVL27DzYU4RvEnKoRVUOY/LH9bo3OQKfgMv+Rgrx7qmpUJFqW/fyQg+wfeR5yo4w2Fqq93fw+xgZ8HN0jaDnCtHqwpV54CK4AZel4wlI25L2x+vQtVBjRd86p/jfs69WVpSoRL8DoawwnI8pv5uFBC4lTwcixJ82HdHfozyQWkqPTW/A8F9eD+11YaGezOv2vzFBXdhzhxVJKAab1Dj18gX/AamjmdoAjPI62G8PIFxFfwFRiCjkzHLbhpjySL4G+xbOcbB4xx/R3fBfbieY+S2FnNaHhx4lwoENzWF49p62AIXqINWIFj/gCNYsGDB/zH8F1f/M45si6u5]AAAAAElFTkSuQmCC> but was:<...AAAFeAQAAAADlUEq3AAA[DD0lEQVR4Xu2ZPXYqMQxGlZMiZZbAUljazNJYyiyBkoIzerqfDDHYcJL6SQWD7WuKb/RnY/57u9rzzBsruLeCeyu4t4J7+z/g3WTuFzucFvu8fG+2fFy/fDuuMdLiZ8FTmG++7vblfjJRtsSS2XHdY+sNKXgCX5j3K/CiEcqG6r6G+KG6FfwW5sEye7SEw+5y2ILfw1CEtfuZIDcjyDPWC34Jx4ffdCa6I8hJk3bkh56ju+AONhnzW6bCh4cWC57Cd5OkuOh9a79acL/a6RxNSxguGjMbsa7EGCFPrDNZ8AROSYMNndX2aTknlTQRv+AJrBwow0WpIq6qfD7wQPy0ggdYOqvy4qlt9EFVDjiL89NLKfgOM8G3s+GpeVxL1UX5+lC7C+7g7FYsW5iWEV35cUd8Wwqew/FJPIfA0hkXPab4TWC9g4KncCu5eWoL23TYOBPr/IJiveAJjG9qWYmRjMjj2k5tYGkFDzDlOHTWPA9GpvxoGA7bvZSCOzjdkDupb5bv8YxvsnbINFnwCEtnDcJucqszzJpyewcFz2AEzqq8ZmlZ2Xjgco+tKX7BA8xE5MD7NXw6pRKj5eFt1Z6CRxhNe52j+/N0UR5MOlbwAOfZlgHlI2sK8UwTKJ0f++eCn13UNOHyRpzSFN1MNaTgEUZgPxHd5+aUW/YusSdrdO4peIRddwWZA3XHp2VqdPsnt+AX8N6W+fOHHIiyxgnk4MQ676ArxwU/w5zTVExQViNFN1RsdazgEf7MyqsgN3zTcNFbdBv2o3PBjzA6tz3I3fYcWFNVZmvBMzgkVf/MfFYRYNJkHoaXgl/BtH0GbBHWWlh1eIv2Bv886vcKHuFm5EfKB4nR2IqntndQ8ByWlvTPVN7W0Kz+U463x8u9gjsYET3/pj0tfN+MKkI33XQu+BV8iQjOVtlpaAhyz04mo3t9rt0FD7ACOUamIEf8PJZ00V3wHPafcswF89GzKrOn4CnM13XPHMjDFeQvorvgsabgopIUKuc49w53BQXf4d9awb0V3FvBvRXcW8G9/Q3+B1f/M451MqUo]AAAAAElFTkSuQmCC>
        at dev.samstevens.totp.util.DataUriEncodingTest.testDataUriEncode(DataUriEncodingTest.java:18)

Explanation :
When reading/write an image, the behavior may (will!) different from one Java version to another.
This is explained here :
https://stackoverflow.com/questions/46790556/java-9-imageio-read-write-gives-a-different-result-than-java-8

Suggested fix (Pull request incoming) : Do not use any image API in this test.
Rational : The purpose of the method Utils.getDataUriForImage being tested is not to read or write an image by following some image format specification. But simply to ensure that a specified byte array (representing an image) gets written using the Data URI specification in base64.

Fix : Rewrite the test just to check that :

  • remove all image loading/writing
  • simply check the output match the expected base 64 data uri syntax.

invalidate old otp values

Hi,

I'm using this library to generate and validate totp values. I generate a new code every 60 seconds,

long currentBucket = Math.floorDiv(new SystemTimeProvider().getTime(), 60);
System.out.println( codeGenerator.generate(secret, currentBucket) );`

However, when I validate the codes, the old codes continue to be valid even after the new codes are generated. Is this the normal case or am I doing something wrong?

timeProvider = new SystemTimeProvider();
codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA1);
verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
verifier.setTimePeriod(60);
verifier.setAllowedTimePeriodDiscrepancy(5);
verifier.isValidCode(secret, totp);

thank you

Improve documentation of generator regarding counter-parameter

Hi & Thanks for this lib. We use the generator & verifier to generate simple expiring OTPs (not looking at the full MFA usecase).

One difficulty we had, was the use of the generator, especially what to pass for the counter parameter. It is easy to figure out that it should be relying on the TimeProvider, but in order of our usecase to work, we need to pass

Math.floorDiv(timeProvider.getTime(), timePeriodInSeconds)

and i guess we're not alone there. This is hard to figure out until you read the code of the verifier.

I was wondering if documentation can be improved here, or (maybe even better) the API can be augmented to be more usable.
For instance, why not have a

public String generate(String key, DefaultCodeVerifier counter) throws CodeGenerationException

so that OTPs can be generated according to the parameters of the verifier (which is weird, because we're not using the interface CodeVerifier here. Maybe extending this with the necessary parameters would help.

Another way to do this would be to have a

public String generate(String key, TimeProvider tprov, int timePeriod) throws CodeGenerationException

But as people look at the method with the min number of params first, some javadoc would really help nevertheless.

What do you think?

PS: i'd be open to create a PR if you want me to.

TotpAutoConfiguration is not loaded automatically by Spring Boot 3

TotpAutoConfiguration is not being loaded automatically by Spring Boot 3.
As a result, all beans defined in TotpAutoConfiguration, such as SecretGenerator, QrDataFactory, QrGenerator, CodeVerifier, are not recognized.

I had to import TotpAutoConfiguration manually in my Spring Boot 3 application:

package com.acme.config;

import dev.samstevens.totp.spring.autoconfigure.TotpAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(TotpAutoConfiguration.class)
public class TotpConfiguration {
}

NtpTimeProvider ignores NTP data

Thank you for this nice library!
I happen to run my app in a few devices that don't have properly sync'd clocks, which caused TOTP codes to fail.

It seemed pretty easy, just using NtpTimeProvider should do it!

Unfortunately it didn't work:
Turns out that NtpTimeProvider currently uses timeInfo.getReturnTime() which is the the local timestamp for the reply -- No NTP data is used at all!

The fixed code looks like this (Maybe ntpOffset could be cached?)

    @Override
    public long getTime() throws TimeProviderException {
        long ntpOffset = 0;
        try {
            TimeInfo timeInfo = client.getTime(ntpHost);
            timeInfo.computeDetails();
            if (timeInfo.getOffset() != null) {
                ntpOffset = timeInfo.getOffset();
                Log.i(TAG, "NTP offset to calculate TOTP is " + ntpOffset + "ms");
            } else {
                Log.w(TAG, "Unable to calculate NTP offset");
            }
        } catch (Exception e) {
            Log.w(TAG, "Failed to get NTP offset", e);
        }

        return (System.currentTimeMillis() + ntpOffset) / 1000;
    }

Unable to verify TOTP using google/microsoft authenticator applications

Hi,

I have been using your library for handling TOTP based QR code generation and validation from last 1 year. It was working successfully with google and microsoft authenticator. Just all of a sudden the TOTP validation is failing which I am failing to understand why?

Below is the code to generate QR code which i scan using authenticator app:

QrData data = new QrData.Builder().label(userDispVal).secret(secret).issuer(label).algorithm(HashingAlgorithm.SHA1).digits(6).period(30).build();
QrGenerator generator = new ZxingPngQrGenerator();
byte[] imageData = generator.generate(data);
String mimeType = generator.getImageMimeType();
String dataUri = getDataUriForImage(imageData, mimeType);

and below is the code to validate the the TOTP code generated by authenticator app:

TimeProvider timeProvider = new SystemTimeProvider();
CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA1);
DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
boolean successful = verifier.isValidCode(secret, code);

The last call is always returning false. It was working earlier like I mentioned.
Any help in this regard will be appreciated.

Thanks,
Ashutosh

TimeProvider that can be instantiated by passing a Clock instance

It would be interesting to have a timeprovider that can be instantiated by passing a clock instance as a parameter.

This would be useful for testing and when working with instances of Clock

    private class ClockTimeProvider implements TimeProvider {

        private Clock clock;

        public ClockTimeProvider(Clock clock) {

            this.clock = clock;
        }

        @Override
        public long getTime() throws TimeProviderException {

            return Instant.now(this.clock).getEpochSecond();
        }
    }

Discussion : why ZXing 3.3.0 ?

Your project's pom.xml declare a dependency to ZXing version 3.3.0 (whose javase module uses JAI Image IO 1.3.1)

ZXing updates are available :

  • 3.3.3 was release in may 2018, it uses JAI Image IO 1.4.0.
  • 3.4.0 was release in may 2019, it uses JAI Image IO 1.4.0.

Is there any rational behind your choice that we should be aware of ?

Storing 2FA status

Howdy!

I understand that I'm kind of abusing the issue system by using it to ask this question, however I am at my wits end with this and need to be pointed in the right direction. Maybe you can help.

I'm using your Library to implement 2FA in an Android app, alongside Firebase Auth as the primary authentication method. When I start up my application, checking if the user is currently logged in is quite easy. However, I can't figure out how I might save the fact that the user has passed the TOTP check since they logged in.

This is my first time trying to implement user authentication in a project if that wasn't immediately obvious. I've been trying to come up with my own solution to this for a few days, but I'm at the point where I need to ask for help, and Stack Overflow hasn't been able to supply any.

Increase recovery codes entropy

Base32 is currently being used to generate recovery code. Therefore, the alphabet that makes up the generation of the recovery code is 32 characters (from A to Z and from 2 to 7). As the key size is 10 characters, the total number of keys possible in this alphabet is is 32^10.

The calculation of the entropy "x" is calculated as follows: x = log(32^10)/log(2) = 50
The entropy of current code is 50 bits.

Because recovery codes MUST be kept simple to read and enter by end user, it is important to keep a simple character set, without being very long.
Suggestion for alternative recovery code implementation :

  • use a larger set of characters, yet still simple to read/enter : numbers and lower case characters from latin alphabet (for a total of 36 possible characters)
  • increase code length, from 10 to 16
  • continue to split code in groups, separated with dashes

The entropy using those settings would be log(36^16)/log(2) = 82.7, matching security requirements.
This would generate recovery code like "phuo-1dm4-647k-8i2n". This same format is for example being used by DropBox for its MFA.

It could be argued that 0 and 1 should be removed from the character set to prevent confusion with O and L/I when read/entered. However this would reduce the entropy to 81, unless length is increased to 17 or more. I think with a proper font this is not required.

(Incoming pull request)

Feature Request: Configurable QR Code color

It would be a great addition to allow users to create a different colored QR codes - this is especially useful if you plan to show the code in dark mode for instance and want to invert the colors.

From looking at the code, it seems the used MatrixToImageWriter supports passing in a custom MatrixToImageConfig object, which defines the ON and OFF colors used to create the image.
See https://zxing.github.io/zxing/apidocs/com/google/zxing/client/j2se/MatrixToImageWriter.html#writeToStream-com.google.zxing.common.BitMatrix-java.lang.String-java.io.OutputStream-com.google.zxing.client.j2se.MatrixToImageConfig- and https://zxing.github.io/zxing/apidocs/com/google/zxing/client/j2se/MatrixToImageConfig.html

Issue on code verification

Hi,

I am trying to implement your library for very simple use.
Only code generation from secret and then checking it.

But this verification always fail.

I use the SystemTimeProvider and all defaults parameters.

Could you help me with it ?

Cordially.

problem validating code with non-default values

CodeVerifier works all fine for me with the default values for HashingAlgorithm = SHA1, and time period validity = 30.
With anyhing different, however, CodeVerifier is failing to verify codes for me.

For my testing i use both - your CodeGenerator.generate() and user-side MFA mobile apps (eg, RedHat FreeOTP 1.5, Duo Mobile 3.30).

As long as your CodeGenerator will still validate its codes with SHA256 or SHA512 with time period 30, when it comes to a period validity any other than 30 it seems to always fail.
When I use the above mentioned mobile apps they seem to work ONLY with the default SHA1 and time period validity of 30. Anything different fails for me.

I do use the same Hashing and Period validity values when creating QRCode and then CodeGenerator/CodeVerifier for verification.

How to confirm whether the QR code has been scanned?

Hi,

This is a great package! Amazing.

I just have one question.
How can I confirm whether the QR code has been scanned?
In case that the user didn't scan the QR, but the server side has registered the secret key, then both side(server side and the MFA APP) couldn't sync the key info. Then the MFA login would fail.
How to make the server side notified that the QR has been scanned and the key has registered on the MFA APP?

Many thanks.

Discussion : Hashing algorithm other than SHA-1

Were you able to successfully use hashing algorithm other than SHA-1 with any application ?
I could not...

I have tested the following application and I could not make it work :

On Android :

On Windows :

Do you known any application supporting hashing algorithm other than SHA-1 ?

Null pointer Exception while calling ZxingPngQrGenerator generate method

Code:-
public byte[] generateQrCode(Users user) throws QrGenerationException {
SecretGenerator secretGenerator = new DefaultSecretGenerator();
String secret = secretGenerator.generate();
QrData data = new QrData.Builder().label(user.getEmail()).secret(secret).issuer("ABC")
.algorithm(HashingAlgorithm.SHA1) // More on this below
.digits(6).period(30).build();
QrGenerator generator = new ZxingPngQrGenerator();
return generator.generate(data);
}

Error:-
5:52:24.618 [http-nio-8090-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler processing failed; nested exception is java.lang.ExceptionInInitializerError] with root cause
java.lang.NullPointerException: null
at javax.imageio.spi.IIORegistry.getDefaultInstance(IIORegistry.java:156)
at javax.imageio.ImageIO.(ImageIO.java:66)
at com.google.zxing.client.j2se.MatrixToImageWriter.writeToStream(MatrixToImageWriter.java:159)
at com.google.zxing.client.j2se.MatrixToImageWriter.writeToStream(MatrixToImageWriter.java:144)
at dev.samstevens.totp.qr.ZxingPngQrGenerator.generate(ZxingPngQrGenerator.java:41)
at com.pfy.newservice.AuthenticatorService.generateQrCode(AuthenticatorService.java:30)
at com.pfy.newservice.AuthenticatorService$$FastClassBySpringCGLIB$$c99c7e83.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
at com.pfy.newservice.AuthenticatorService$$EnhancerBySpringCGLIB$$b8d82602.generateQrCode()
at com.pfy.rest.AuthenticatorResource.generateQrCode(AuthenticatorResource.java:46)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at com.pfy.CORSFilter.doFilter(CORSFilter.java:35)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:522)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1095)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1502)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1458)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)

Have certain values be configurable

Some values that should be configurable are currently hardcoded and cannot be changed. Allow these values to be changed by consumers to make the library more flexible.

The values that should be configurable are:

  • the NtpTimeProvider timeout
  • the algorithm that is used when generating codes
  • the number of digits in the codes produced
  • the number of seconds in each "time bucket"
  • the number of time buckets before and after the current time bucket to generate valid codes for when verifying a code (time discrepency)

CodeGenerator interface not accessible

The sample code in the documentation indicates the following usage :

CodeGenerator codeGenerator = new DefaultCodeGenerator();

Problem : CodeGenerator interface is package protected, thus it cannot be used.

(Obvious) Workaround :
DefaultCodeGenerator codeGenerator = new DefaultCodeGenerator();

Configure border size

Dear,
It would be great if user can specify the border size of the generated QR.
Good library ;)

Basic usage help needed

Hi

Sorry for asking here if it's not the place but I tried stackoverflow with no response.
I'm trying to validate a token generated by an App (FortiToken Mobile if it matters) but the validator keeps returning false to me.
I went over the usage section in the home page here several times but I can't see what I missed.
Here is the code I play with - It has the qr code generation, validation, and trying to get a token so I can email the user for him to enter it back.

Any help/direction would be great
Thanks

`

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	// TODO Auto-generated method stub
	response.getWriter().append("Served at: ").append(request.getContextPath());
	
	
	//SecretGenerator secretGenerator = new DefaultSecretGenerator();
	//String secret = secretGenerator.generate();
	String secret = "XAFXRG3TNMLHENVAQTD5ZJOTC2MHTIVE";
	
	// ========================================
	// QR code generation section
	// ========================================
	QrData data = new QrData.Builder()
			   .label("[email protected]")
			   .secret(secret)
			   .issuer("PORTAL")
			   .algorithm(HashingAlgorithm.SHA256) // More on this below
			   .digits(6)
			   .period(60)
			   .build();

	try {
		QrGenerator generator = new ZxingPngQrGenerator();
		byte[] imageData = generator.generate(data);
		String mimeType = generator.getImageMimeType();
		String dataUri = getDataUriForImage(imageData, mimeType);
		response.getWriter().append("\r\ndataUri: " + dataUri);//.append(request.getContextPath());
	} catch (QrGenerationException ex) {
		// TODO Auto-generated catch block
		ex.printStackTrace();
	}

	
	
	
	CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA256);
	// ========================================
	// Generating a code to email the user
	// ========================================
	
	try {
	String codeToCheck = codeGenerator.generate(secret, 1);	// what should come here instead of the "1"
	// codeToCheck is never the ok - I guess because of the second parameter which I don't know what to send.
	response.getWriter().append("\r\ncodeToCheck: " + codeToCheck);//.append(request.getContextPath());
	} catch (CodeGenerationException ex){
		ex.printStackTrace();
	}
	
	// ========================================
	// Getting code from the url and checking it
	// ========================================
	String code = request.getQueryString().replace("code=", "");
	response.getWriter().append("\r\nCode: " + code);//.append(request.getContextPath());
	
	TimeProvider timeProvider = new SystemTimeProvider();
	DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
	verifier.setTimePeriod(60);
	verifier.setAllowedTimePeriodDiscrepancy(2);

	// secret = the shared secret for the user
	// code = the code submitted by the user
	boolean successful = verifier.isValidCode(secret, code);
	if (successful) System.out.println(successful);
	response.getWriter().append("\r\nResult: " + successful);//.append(request.getContextPath());

	
}

`

Display Secret with authenticator-compatible/matching-hashing-algorithm string

Hello and thank you very much for this library, it works like a charm! :)

I have a question though: I would like to display an authenticator configuration key below the QR Code for mobile users (it's easier for them to copy paste the key as they can't scan the QR Code with the same device they are browsing the app from), but I use SHA 256 for the QR Code generation and code verification. So:

  • when I scan the QR Code and type the bare secret in the same authenticator app, the OTPs don't match
  • when I 256-hash the secret and try to import it in the authenticator, it tells me it's not valid

Is there something special to implement to have an authenticator configuration key that matches the QR Code?
Thanks a lot in advance :D

(Sorry if this is not the place to discuss such matters)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.