Skip to content

Commit

Permalink
Merge branch 'release/0.6.0'
Browse files Browse the repository at this point in the history
emcrisostomo committed Feb 22, 2016
2 parents 7051a9a + 55f4aed commit ab86a29
Showing 5 changed files with 190 additions and 65 deletions.
29 changes: 29 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# Copyright (c) 2013 Warren Strange
# Copyright (c) 2014-2015 Enrico M. Crisostomo
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the author nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
SUBDIRS = doc
8 changes: 4 additions & 4 deletions doc/totp.texi
Original file line number Diff line number Diff line change
@@ -112,14 +112,14 @@ authentication. No more, no less.

There exist more complex and comprehensive solutions, and if you need something
more than being able to perform @acronym{TOTP} authentication, chances are you
should assess whether you should use this library or some other product.
should assess whether to use this library or some other product.

On the other hand, if what you are looking @emph{is} performing @acronym{TOTP}
authentication, then this library will provide an easy to use @acronym{API} and
a @emph{very} compact library: currently, the size of the library @acronym{JAR}
archive is smaller than 20 @acronym{KB}. Most importantly, this library has
very few@footnote{This library's dependencies are compact as well and do not
pull-in a chain of dependencies themselves.} dependencies:
archive is smaller than 20 kB. Most importantly, this library has very
few@footnote{This library's dependencies are compact as well and do not pull-in
a chain of dependencies themselves.} dependencies:

@itemize
@item
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@

<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>0.5.0</version>
<version>0.6.0</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
159 changes: 108 additions & 51 deletions src/main/java/com/warrenstrange/googleauth/GoogleAuthenticator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* Copyright (c) 2014-2016 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -47,13 +47,13 @@
* implementation of such algorithm in its Google Authenticator application.
* <p/>
* This class lets users create a new 16-bit base32-encoded secret key with
* the validation code calculated at time=0 (the UNIX epoch) and the URL of a
* Google-provided QR barcode to let an user load the generated information into
* Google Authenticator.
* the validation code calculated at {@code time = 0} (the UNIX epoch) and the
* URL of a Google-provided QR barcode to let an user load the generated
* information into Google Authenticator.
* <p/>
* The random number generator used by this class uses the default algorithm and provider.
* Users can override them by setting the following system properties to the algorithm
* and provider name of their choice:
* The random number generator used by this class uses the default algorithm and
* provider. Users can override them by setting the following system properties
* to the algorithm and provider name of their choice:
* <ul>
* <li>{@link #RNG_ALGORITHM}.</li>
* <li>{@link #RNG_ALGORITHM_PROVIDER}.</li>
@@ -73,7 +73,8 @@
* @see <a href="http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt" />
* @since 0.3.0
*/
public final class GoogleAuthenticator implements IGoogleAuthenticator {
public final class GoogleAuthenticator implements IGoogleAuthenticator
{

/**
* The system property to specify the random number generator algorithm to use.
@@ -169,12 +170,15 @@ public final class GoogleAuthenticator implements IGoogleAuthenticator {
getRandomNumberAlgorithm(),
getRandomNumberAlgorithmProvider());

public GoogleAuthenticator() {
public GoogleAuthenticator()
{
config = new GoogleAuthenticatorConfig();
}

public GoogleAuthenticator(GoogleAuthenticatorConfig config) {
if (config == null) {
public GoogleAuthenticator(GoogleAuthenticatorConfig config)
{
if (config == null)
{
throw new IllegalArgumentException("Configuration cannot be null.");
}

@@ -185,7 +189,8 @@ public GoogleAuthenticator(GoogleAuthenticatorConfig config) {
* @return the random number generator algorithm.
* @since 0.5.0
*/
private String getRandomNumberAlgorithm() {
private String getRandomNumberAlgorithm()
{
return System.getProperty(
RNG_ALGORITHM,
DEFAULT_RANDOM_NUMBER_ALGORITHM);
@@ -195,7 +200,8 @@ private String getRandomNumberAlgorithm() {
* @return the random number generator algorithm provider.
* @since 0.5.0
*/
private String getRandomNumberAlgorithmProvider() {
private String getRandomNumberAlgorithmProvider()
{
return System.getProperty(
RNG_ALGORITHM_PROVIDER,
DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER);
@@ -210,22 +216,25 @@ private String getRandomNumberAlgorithmProvider() {
* @return the validation code for the provided key at the specified instant
* of time.
*/
int calculateCode(byte[] key, long tm) {
int calculateCode(byte[] key, long tm)
{
// Allocating an array of bytes to represent the specified instant
// of time.
byte[] data = new byte[8];
long value = tm;

// Converting the instant of time from the long representation to a
// big-endian array of bytes (RFC4226, 5.2. Description).
for (int i = 8; i-- > 0; value >>>= 8) {
for (int i = 8; i-- > 0; value >>>= 8)
{
data[i] = (byte) value;
}

// Building the secret key specification for the HmacSHA1 algorithm.
SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);

try {
try
{
// Getting an HmacSHA1 algorithm implementation from the JCE.
Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);

@@ -243,7 +252,8 @@ int calculateCode(byte[] key, long tm) {
// and we need 32 unsigned bits).
long truncatedHash = 0;

for (int i = 0; i < 4; ++i) {
for (int i = 0; i < 4; ++i)
{
truncatedHash <<= 8;

// Java bytes are signed but we need an unsigned integer:
@@ -258,7 +268,9 @@ int calculateCode(byte[] key, long tm) {

// Returning the validation code to the caller.
return (int) truncatedHash;
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
}
catch (NoSuchAlgorithmException | InvalidKeyException ex)
{
// Logging the exception.
LOGGER.log(Level.SEVERE, ex.getMessage(), ex);

@@ -284,11 +296,13 @@ private boolean checkCode(
String secret,
long code,
long timestamp,
int window) {
int window)
{
byte[] decodedKey;

// Decoding the secret key to get its raw byte representation.
switch (config.getKeyRepresentation()) {
switch (config.getKeyRepresentation())
{
case BASE32:
Base32 codec32 = new Base32();
decodedKey = codec32.decode(secret);
@@ -308,12 +322,14 @@ private boolean checkCode(
// Calculating the verification code of the given key in each of the
// time intervals and returning true if the provided code is equal to
// one of them.
for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
for (int i = -((window - 1) / 2); i <= window / 2; ++i)
{
// Calculating the verification code for the current time interval.
long hash = calculateCode(decodedKey, timeWindow + i);

// Checking if the provided code is equal to the calculated one.
if (hash == code) {
if (hash == code)
{
// The verification code is valid.
return true;
}
@@ -324,7 +340,8 @@ private boolean checkCode(
}

@Override
public GoogleAuthenticatorKey createCredentials() {
public GoogleAuthenticatorKey createCredentials()
{

// Allocating a buffer sufficiently large to hold the bytes required by
// the secret key and the scratch codes.
@@ -350,9 +367,11 @@ public GoogleAuthenticatorKey createCredentials() {
}

@Override
public GoogleAuthenticatorKey createCredentials(String userName) {
public GoogleAuthenticatorKey createCredentials(String userName)
{
// Further validation will be performed by the configured provider.
if (userName == null) {
if (userName == null)
{
throw new IllegalArgumentException("User name cannot be null.");
}

@@ -368,20 +387,25 @@ public GoogleAuthenticatorKey createCredentials(String userName) {
return key;
}

private List<Integer> calculateScratchCodes(byte[] buffer) {
private List<Integer> calculateScratchCodes(byte[] buffer)
{
List<Integer> scratchCodes = new ArrayList<>();

while (scratchCodes.size() < SCRATCH_CODES) {
while (scratchCodes.size() < SCRATCH_CODES)
{
byte[] scratchCodeBuffer = Arrays.copyOfRange(
buffer,
SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size(),
SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size() + BYTES_PER_SCRATCH_CODE);

int scratchCode = calculateScratchCode(scratchCodeBuffer);

if (scratchCode != SCRATCH_CODE_INVALID) {
if (scratchCode != SCRATCH_CODE_INVALID)
{
scratchCodes.add(scratchCode);
} else {
}
else
{
scratchCodes.add(generateScratchCode());
}
}
@@ -397,8 +421,10 @@ private List<Integer> calculateScratchCodes(byte[] buffer) {
* <code>#BYTES_PER_SCRATCH_CODE</code>.
* @return the scratch code.
*/
private int calculateScratchCode(byte[] scratchCodeBuffer) {
if (scratchCodeBuffer.length < BYTES_PER_SCRATCH_CODE) {
private int calculateScratchCode(byte[] scratchCodeBuffer)
{
if (scratchCodeBuffer.length < BYTES_PER_SCRATCH_CODE)
{
throw new IllegalArgumentException(
String.format(
"The provided random byte buffer is too small: %d.",
@@ -407,22 +433,27 @@ private int calculateScratchCode(byte[] scratchCodeBuffer) {

int scratchCode = 0;

for (int i = 0; i < BYTES_PER_SCRATCH_CODE; ++i) {
for (int i = 0; i < BYTES_PER_SCRATCH_CODE; ++i)
{
scratchCode = (scratchCode << 8) + (scratchCodeBuffer[i] & 0xff);
}

scratchCode = (scratchCode & 0x7FFFFFFF) % SCRATCH_CODE_MODULUS;

// Accept the scratch code only if it has exactly
// SCRATCH_CODE_LENGTH digits.
if (validateScratchCode(scratchCode)) {
if (validateScratchCode(scratchCode))
{
return scratchCode;
} else {
}
else
{
return SCRATCH_CODE_INVALID;
}
}

/* package */ boolean validateScratchCode(int scratchCode) {
/* package */ boolean validateScratchCode(int scratchCode)
{
return (scratchCode >= SCRATCH_CODE_MODULUS / 10);
}

@@ -434,14 +465,17 @@ private int calculateScratchCode(byte[] scratchCodeBuffer) {
*
* @return A valid scratch code.
*/
private int generateScratchCode() {
while (true) {
private int generateScratchCode()
{
while (true)
{
byte[] scratchCodeBuffer = new byte[BYTES_PER_SCRATCH_CODE];
secureRandom.nextBytes(scratchCodeBuffer);

int scratchCode = calculateScratchCode(scratchCodeBuffer);

if (scratchCode != SCRATCH_CODE_INVALID) {
if (scratchCode != SCRATCH_CODE_INVALID)
{
return scratchCode;
}
}
@@ -453,7 +487,8 @@ private int generateScratchCode() {
* @param secretKey The secret key to use.
* @return the validation code at time 0.
*/
private int calculateValidationCode(byte[] secretKey) {
private int calculateValidationCode(byte[] secretKey)
{
return calculateCode(secretKey, 0);
}

@@ -463,8 +498,10 @@ private int calculateValidationCode(byte[] secretKey) {
* @param secretKey a random byte buffer.
* @return the secret key.
*/
private String calculateSecretKey(byte[] secretKey) {
switch (config.getKeyRepresentation()) {
private String calculateSecretKey(byte[] secretKey)
{
switch (config.getKeyRepresentation())
{
case BASE32:
return new Base32().encodeToString(secretKey);
case BASE64:
@@ -476,32 +513,48 @@ private String calculateSecretKey(byte[] secretKey) {

@Override
public boolean authorize(String secret, int verificationCode)
throws GoogleAuthenticatorException {
throws GoogleAuthenticatorException
{
return authorize(secret, verificationCode, new Date().getTime());
}

@Override
public boolean authorize(String secret, int verificationCode, long time)
throws GoogleAuthenticatorException
{
// Checking user input and failing if the secret key was not provided.
if (secret == null) {
if (secret == null)
{
throw new IllegalArgumentException("Secret cannot be null.");
}

// Checking if the verification code is between the legal bounds.
if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus()) {
if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus())
{
return false;
}

// Checking the validation code using the current UNIX time.
return checkCode(
secret,
verificationCode,
new Date().getTime(),
time,
this.config.getWindowSize());
}

@Override
public boolean authorizeUser(String userName, int verificationCode)
throws GoogleAuthenticatorException {
throws GoogleAuthenticatorException
{
return authorizeUser(userName, verificationCode, new Date().getTime());
}

@Override
public boolean authorizeUser(String userName, int verificationCode, long time) throws GoogleAuthenticatorException
{
ICredentialRepository repository = getValidCredentialRepository();

return authorize(repository.getSecretKey(userName), verificationCode);
return authorize(repository.getSecretKey(userName), verificationCode, time);
}

/**
@@ -512,10 +565,12 @@ public boolean authorizeUser(String userName, int verificationCode)
* @throws java.lang.UnsupportedOperationException if no valid service is
* found.
*/
private ICredentialRepository getValidCredentialRepository() {
private ICredentialRepository getValidCredentialRepository()
{
ICredentialRepository repository = getCredentialRepository();

if (repository == null) {
if (repository == null)
{
throw new UnsupportedOperationException(
String.format("An instance of the %s service must be " +
"configured in order to use this feature.",
@@ -534,12 +589,14 @@ private ICredentialRepository getValidCredentialRepository() {
* @return the first registered ICredentialRepository or <code>null</code>
* if none is found.
*/
private ICredentialRepository getCredentialRepository() {
private ICredentialRepository getCredentialRepository()
{
ServiceLoader<ICredentialRepository> loader =
ServiceLoader.load(ICredentialRepository.class);

//noinspection LoopStatementThatDoesntLoop
for (ICredentialRepository repository : loader) {
for (ICredentialRepository repository : loader)
{
return repository;
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* Copyright (c) 2014-2016 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -34,7 +34,8 @@
* Google Authenticator library interface.
*/
@SuppressWarnings("UnusedDeclaration")
public interface IGoogleAuthenticator {
public interface IGoogleAuthenticator
{
/**
* This method generates a new set of credentials including:
* <ol>
@@ -64,11 +65,6 @@ public interface IGoogleAuthenticator {

/**
* Checks a verification code against a secret key using the current time.
* The algorithm also checks in a time window whose size determined by the
* <code>windowSize</code> property of this class.
* <p/>
* The default value of 30 seconds recommended by RFC 6238 is used for the
* interval size.
*
* @param secret the Base32 encoded secret key.
* @param verificationCode the verification code.
@@ -79,14 +75,38 @@ public interface IGoogleAuthenticator {
* The only failures that should occur
* are related with the cryptographic
* functions provided by the JCE.
* @see #authorize(String, int, long)
*/
boolean authorize(String secret, int verificationCode)
throws GoogleAuthenticatorException;

/**
* Checks a verification code against a secret key using the specified time.
* The algorithm also checks in a time window whose size determined by the
* {@code windowSize} property of this class.
* <p/>
* The default value of 30 seconds recommended by RFC 6238 is used for the
* interval size.
*
* @param secret The Base32 encoded secret key.
* @param verificationCode The verification code.
* @param time The time to use to calculate the TOTP password..
* @return {@code true} if the validation code is valid, {@code false}
* otherwise.
* @throws GoogleAuthenticatorException if a failure occurs during the
* calculation of the validation code.
* The only failures that should occur
* are related with the cryptographic
* functions provided by the JCE.
*/
boolean authorize(String secret, int verificationCode, long time)
throws GoogleAuthenticatorException;

/**
* This method validates a verification code of the specified user whose
* private key is retrieved from the configured credential repository. This
* method delegates the validation to the <code>#authorize</code> method.
* private key is retrieved from the configured credential repository using
* the current time. This method delegates the validation to the
* {@link #authorizeUser(String, int, long)}.
*
* @param userName The user whose verification code is to be
* validated.
@@ -98,4 +118,23 @@ boolean authorize(String secret, int verificationCode)
*/
boolean authorizeUser(String userName, int verificationCode)
throws GoogleAuthenticatorException;

/**
* This method validates a verification code of the specified user whose
* private key is retrieved from the configured credential repository. This
* method delegates the validation to the
* {@link #authorize(String, int, long)} method.
*
* @param userName The user whose verification code is to be
* validated.
* @param verificationCode The validation code.
* @param time The time to use to calculate the TOTP password.
* @return <code>true</code> if the validation code is valid,
* <code>false</code> otherwise.
* @throws GoogleAuthenticatorException if an unexpected error occurs.
* @see #authorize(String, int)
*/
boolean authorizeUser(String userName, int verificationCode, long time)
throws GoogleAuthenticatorException;

}

0 comments on commit ab86a29

Please sign in to comment.