GoogleAuthenticator.java
package sk.iway.iwcm.system.googleauth;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
/**
* GoogleAuthenticator.java
*
*@Title webjet8
*@Company Interway s.r.o. (www.interway.sk)
*@Copyright Interway s.r.o. (c) 2001-2017
*@author $Author: jeeff mminda $
*@version $Revision: 1.3 $
*@created Date: Jun 5, 2017 9:41:40 AM
*@modified $Date: 2004/08/16 06:26:11 $
*/
/*this class implements the functionality described in RFC 6238 (TOTP: Time
* based one-time password algorithm) and has been tested again Google's
* 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 {@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:
* <ul>
* <li>{@link #RNG_ALGORITHM}.</li>
* <li>{@link #RNG_ALGORITHM_PROVIDER}.</li>
* </ul>
* <p/>
* This class does not store in any way either the generated keys nor the keys
* passed during the authorization process.
* <p/>
* Java Server side class for Google Authenticator's TOTP generator was inspired
* by an author's blog post.
*
* @author Enrico M. Crisostomo
* @author Warren Strange
* @version 0.5.0
* @see <a href="http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html" />
* @see <a href="http://code.google.com/p/google-authenticator" />
* @see <a href="http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt" />
* @since 0.3.0
*/
public final class GoogleAuthenticator implements IGoogleAuthenticator
{
/**
* The system property to specify the random number generator algorithm to use.
*
* @since 0.5.0
*/
public static final String RNG_ALGORITHM = "com.warrenstrange.googleauth.rng.algorithm";
/**
* The system property to specify the random number generator provider to use.
*
* @since 0.5.0
*/
public static final String RNG_ALGORITHM_PROVIDER = "com.warrenstrange.googleauth.rng.algorithmProvider";
/**
* The logger for this class.
*/
private static final Logger LOGGER = Logger.getLogger(GoogleAuthenticator.class.getName());
/**
* The number of bits of a secret key in binary form. Since the Base32
* encoding with 8 bit characters introduces an 160% overhead, we just need
* 80 bits (10 bytes) to generate a 16 bytes Base32-encoded secret key.
*/
private static final int SECRET_BITS = 80;
/**
* Number of scratch codes to generate during the key generation.
* We are using Google's default of providing 5 scratch codes.
*/
private static final int SCRATCH_CODES = 5;
/**
* Number of digits of a scratch code represented as a decimal integer.
*/
private static final int SCRATCH_CODE_LENGTH = 8;
/**
* Modulus used to truncate the scratch code.
*/
public static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH);
/**
* Magic number representing an invalid scratch code.
*/
private static final int SCRATCH_CODE_INVALID = -1;
/**
* Length in bytes of each scratch code. We're using Google's default of
* using 4 bytes per scratch code.
*/
private static final int BYTES_PER_SCRATCH_CODE = 4;
/**
* The default SecureRandom algorithm to use if none is specified.
*
* @see java.security.SecureRandom#getInstance(String)
* @since 0.5.0
*/
@SuppressWarnings("SpellCheckingInspection")
private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
/**
* The default random number algorithm provider to use if none is specified.
*
* @see java.security.SecureRandom#getInstance(String)
* @since 0.5.0
*/
private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
/**
* Cryptographic hash function used to calculate the HMAC (Hash-based
* Message Authentication Code). This implementation uses the SHA1 hash
* function.
*/
private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
/**
* The configuration used by the current instance.
*/
private final GoogleAuthenticatorConfig config;
/**
* The internal SecureRandom instance used by this class. Since Java 7
* {@link Random} instances are required to be thread-safe, no synchronisation is
* required in the methods of this class using this instance. Thread-safety
* of this class was a de-facto standard in previous versions of Java so
* that it is expected to work correctly in previous versions of the Java
* platform as well.
*/
private ReseedingSecureRandom secureRandom = new ReseedingSecureRandom(
getRandomNumberAlgorithm(),
getRandomNumberAlgorithmProvider());
private ICredentialRepository credentialRepository;
private boolean credentialRepositorySearched;
public GoogleAuthenticator()
{
config = new GoogleAuthenticatorConfig();
}
public GoogleAuthenticator(GoogleAuthenticatorConfig config)
{
if (config == null)
{
throw new IllegalArgumentException("Configuration cannot be null.");
}
this.config = config;
}
/**
* @return the random number generator algorithm.
* @since 0.5.0
*/
private String getRandomNumberAlgorithm()
{
return System.getProperty(
RNG_ALGORITHM,
DEFAULT_RANDOM_NUMBER_ALGORITHM);
}
/**
* @return the random number generator algorithm provider.
* @since 0.5.0
*/
private String getRandomNumberAlgorithmProvider()
{
return System.getProperty(
RNG_ALGORITHM_PROVIDER,
DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER);
}
/**
* Calculates the verification code of the provided key at the specified
* instant of time using the algorithm specified in RFC 6238.
*
* @param key the secret key in binary format.
* @param tm the instant of time.
* @return the validation code for the provided key at the specified instant
* of time.
*/
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)
{
data[i] = (byte) value;
}
// Building the secret key specification for the HmacSHA1 algorithm.
SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
try
{
// Getting an HmacSHA1 algorithm implementation from the JCE.
Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
// Initializing the MAC algorithm.
mac.init(signKey);
// Processing the instant of time and getting the encrypted data.
byte[] hash = mac.doFinal(data);
// Building the validation code performing dynamic truncation
// (RFC4226, 5.3. Generating an HOTP value)
int offset = hash[hash.length - 1] & 0xF;
// We are using a long because Java hasn't got an unsigned integer type
// and we need 32 unsigned bits).
long truncatedHash = 0;
for (int i = 0; i < 4; ++i)
{
truncatedHash <<= 8;
// Java bytes are signed but we need an unsigned integer:
// cleaning off all but the LSB.
truncatedHash |= (hash[offset + i] & 0xFF);
}
// Clean bits higher than the 32nd (inclusive) and calculate the
// module with the maximum validation code value.
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= config.getKeyModulus();
// Returning the validation code to the caller.
return (int) truncatedHash;
}
catch (NoSuchAlgorithmException | InvalidKeyException ex)
{
// Logging the exception.
LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
// We're not disclosing internal error details to our clients.
throw new GoogleAuthenticatorException("The operation cannot be "
+ "performed now.");
}
}
private long getTimeWindowFromTime(long time)
{
return time / this.config.getTimeStepSizeInMillis();
}
/**
* This method implements the algorithm specified in RFC 6238 to check if a
* validation code is valid in a given instant of time for the given secret
* key.
*
* @param secret the Base32 encoded secret key.
* @param code the code to validate.
* @param timestamp the instant of time to use during the validation process.
* @param window the window size to use during the validation process.
* @return <code>true</code> if the validation code is valid,
* <code>false</code> otherwise.
*/
private boolean checkCode(
String secret,
long code,
long timestamp,
int window)
{
byte[] decodedKey = decodeSecret(secret);
// convert unix time into a 30 second "window" as specified by the
// TOTP specification. Using Google's default interval of 30 seconds.
final long timeWindow = getTimeWindowFromTime(timestamp);
// 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)
{
// 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)
{
// The verification code is valid.
return true;
}
}
// The verification code is invalid.
return false;
}
private byte[] decodeSecret(String secret)
{
// Decoding the secret key to get its raw byte representation.
switch (config.getKeyRepresentation())
{
case BASE32:
Base32 codec32 = new Base32();
return codec32.decode(secret);
case BASE64:
Base64 codec64 = new Base64();
return codec64.decode(secret);
default:
throw new IllegalArgumentException("Unknown key representation type.");
}
}
@Override
public GoogleAuthenticatorKey createCredentials()
{
// Allocating a buffer sufficiently large to hold the bytes required by
// the secret key and the scratch codes.
byte[] buffer =
new byte[SECRET_BITS / 8 + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE];
secureRandom.nextBytes(buffer);
// Extracting the bytes making up the secret key.
byte[] secretKey = Arrays.copyOf(buffer, SECRET_BITS / 8);
String generatedKey = calculateSecretKey(secretKey);
// Generating the verification code at time = 0.
int validationCode = calculateValidationCode(secretKey);
// Calculate scratch codes
List<Integer> scratchCodes = calculateScratchCodes(buffer);
return new GoogleAuthenticatorKey(
generatedKey,
validationCode,
scratchCodes);
}
@Override
public GoogleAuthenticatorKey createCredentials(String userName)
{
// Further validation will be performed by the configured provider.
if (userName == null)
{
throw new IllegalArgumentException("User name cannot be null.");
}
GoogleAuthenticatorKey key = createCredentials();
ICredentialRepository repository = getValidCredentialRepository();
repository.saveUserCredentials(
userName,
key.getKey(),
key.getVerificationCode(),
key.getScratchCodes());
return key;
}
private List<Integer> calculateScratchCodes(byte[] buffer)
{
List<Integer> scratchCodes = new ArrayList<>();
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)
{
scratchCodes.add(scratchCode);
}
else
{
scratchCodes.add(generateScratchCode());
}
}
return scratchCodes;
}
/**
* This method calculates a scratch code from a random byte buffer of
* suitable size <code>#BYTES_PER_SCRATCH_CODE</code>.
*
* @param scratchCodeBuffer a random byte buffer whose minimum size is
* <code>#BYTES_PER_SCRATCH_CODE</code>.
* @return the 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.",
scratchCodeBuffer.length));
}
int scratchCode = 0;
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))
{
return scratchCode;
}
else
{
return SCRATCH_CODE_INVALID;
}
}
/* package */ boolean validateScratchCode(int scratchCode)
{
return (scratchCode >= SCRATCH_CODE_MODULUS / 10);
}
/**
* This method creates a new random byte buffer from which a new scratch
* code is generated. This function is invoked if a scratch code generated
* from the main buffer is invalid because it does not satisfy the scratch
* code restrictions.
*
* @return A valid scratch code.
*/
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)
{
return scratchCode;
}
}
}
/**
* This method calculates the validation code at time 0.
*
* @param secretKey The secret key to use.
* @return the validation code at time 0.
*/
private int calculateValidationCode(byte[] secretKey)
{
return calculateCode(secretKey, 0);
}
public int getTotpPassword(String secret)
{
return getTotpPassword(secret, new Date().getTime());
}
public int getTotpPassword(String secret, long time)
{
return calculateCode(decodeSecret(secret), getTimeWindowFromTime(time));
}
public int getTotpPasswordOfUser(String userName)
{
return getTotpPasswordOfUser(userName, new Date().getTime());
}
public int getTotpPasswordOfUser(String userName, long time)
{
ICredentialRepository repository = getValidCredentialRepository();
return calculateCode(
decodeSecret(repository.getSecretKey(userName)),
getTimeWindowFromTime(time));
}
/**
* This method calculates the secret key given a random byte buffer.
*
* @param secretKey a random byte buffer.
* @return the secret key.
*/
private String calculateSecretKey(byte[] secretKey)
{
switch (config.getKeyRepresentation())
{
case BASE32:
return new Base32().encodeToString(secretKey);
case BASE64:
return new Base64().encodeToString(secretKey);
default:
throw new IllegalArgumentException("Unknown key representation type.");
}
}
@Override
public boolean authorize(String secret, int verificationCode)
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)
{
throw new IllegalArgumentException("Secret cannot be null.");
}
// Checking if the verification code is between the legal bounds.
if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus())
{
return false;
}
// Checking the validation code using the current UNIX time.
return checkCode(
secret,
verificationCode,
time,
this.config.getWindowSize());
}
@Override
public boolean authorizeUser(String userName, int verificationCode)
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, time);
}
/**
* This method loads the first available and valid ICredentialRepository
* registered using the Java service loader API.
*
* @return the first registered ICredentialRepository.
* @throws java.lang.UnsupportedOperationException if no valid service is
* found.
*/
private ICredentialRepository getValidCredentialRepository()
{
ICredentialRepository repository = getCredentialRepository();
if (repository == null)
{
throw new UnsupportedOperationException(
String.format("An instance of the %s service must be " +
"configured in order to use this feature.",
ICredentialRepository.class.getName()
)
);
}
return repository;
}
/**
* This method loads the first available ICredentialRepository
* registered using the Java service loader API.
*
* @return the first registered ICredentialRepository or <code>null</code>
* if none is found.
*/
public ICredentialRepository getCredentialRepository()
{
if (this.credentialRepositorySearched) return this.credentialRepository;
this.credentialRepositorySearched = true;
ServiceLoader<ICredentialRepository> loader =
ServiceLoader.load(ICredentialRepository.class);
//noinspection LoopStatementThatDoesntLoop
for (ICredentialRepository repository : loader)
{
this.credentialRepository = repository;
break;
}
return this.credentialRepository;
}
@Override
public void setCredentialRepository(ICredentialRepository repository)
{
this.credentialRepository = repository;
this.credentialRepositorySearched = true;
}
}