/* * Copyright (c) 2014-2016 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. */ package com.sismics.util.totp; 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 javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.Base64; /** * 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. *
* 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. * * 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: *true
if the validation code is valid,
* false
otherwise.
*/
private boolean checkCode(String secret, long code, long timestamp, int window) {
// Decoding the secret key to get its raw byte representation.
byte[] decodedKey = decodeKey(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 = timestamp / this.config.getTimeStepSizeInMillis();
// 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;
}
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#BYTES_PER_SCRATCH_CODE
.
*
* @param scratchCodeBuffer a random byte buffer whose minimum size is #BYTES_PER_SCRATCH_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 (scratchCode >= SCRATCH_CODE_MODULUS / 10) {
return scratchCode;
} else {
return SCRATCH_CODE_INVALID;
}
}
/**
* 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);
}
/**
* 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.");
}
}
public boolean authorize(String secret, int verificationCode) throws GoogleAuthenticatorException {
return authorize(secret, verificationCode, new Date().getTime());
}
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());
}
}