From 6e6babd2e3bac73ffc2a6f6c4aee13eda52fe0c0 Mon Sep 17 00:00:00 2001 From: jendib Date: Tue, 22 Mar 2016 22:15:19 +0100 Subject: [PATCH] #84: Import sources from https://github.com/wstrange/GoogleAuth --- docs-core/pom.xml | 1 - .../util/totp/GoogleAuthenticator.java | 456 ++++++++++++++++++ .../util/totp/GoogleAuthenticatorConfig.java | 149 ++++++ .../totp/GoogleAuthenticatorException.java | 61 +++ .../util/totp/GoogleAuthenticatorKey.java | 105 ++++ .../sismics/util/totp/KeyRepresentation.java | 36 ++ .../util/totp/ReseedingSecureRandom.java | 113 +++++ .../sismics/util/TestGoogleAuthenticator.java | 26 + docs-parent/pom.xml | 8 - docs-web/pom.xml | 5 - .../docs/rest/resource/UserResource.java | 5 +- 11 files changed, 948 insertions(+), 17 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java create mode 100644 docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java create mode 100644 docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java create mode 100644 docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java create mode 100644 docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java create mode 100644 docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java create mode 100644 docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java diff --git a/docs-core/pom.xml b/docs-core/pom.xml index ed0673dc..eeb23ba9 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -77,7 +77,6 @@ jbcrypt - org.apache.lucene lucene-core diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java new file mode 100644 index 00000000..c37147f9 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java @@ -0,0 +1,456 @@ +/* + * 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: + *

+ *

+ * This class does not store in any way either the generated keys nor the keys + * passed during the authorization process. + *

+ * 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 + * @see + * @see + * @since 0.3.0 + */ +public final class GoogleAuthenticator { + /** + * 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 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 + */ + 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()); + + 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); + } + + public int calculateCode(String secret, long tm) { + return calculateCode(decodeKey(secret), tm); + } + + /** + * Decode a secret key in raw bytes. + * + * @param secret Secret key + * @return Raw bytes + */ + private byte[] decodeKey(String secret) { + 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."); + } + } + + /** + * 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) { + // We're not disclosing internal error details to our clients. + throw new GoogleAuthenticatorException("The operation cannot be performed now.", ex); + } + } + + /** + * 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 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 scratchCodes = calculateScratchCodes(buffer); + + return new GoogleAuthenticatorKey(generatedKey, validationCode, scratchCodes); + } + + private List calculateScratchCodes(byte[] buffer) { + List 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 #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()); + } +} diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java new file mode 100644 index 00000000..cabc7f63 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java @@ -0,0 +1,149 @@ +/* + * 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. + */ + +package com.sismics.util.totp; + +import java.util.concurrent.TimeUnit; + +public class GoogleAuthenticatorConfig { + private long timeStepSizeInMillis = TimeUnit.SECONDS.toMillis(30); + private int windowSize = 3; + private int codeDigits = 6; + private int keyModulus = (int) Math.pow(10, codeDigits); + private KeyRepresentation keyRepresentation = KeyRepresentation.BASE32; + + /** + * Returns the key module. + * + * @return the key module. + */ + public int getKeyModulus() { + return keyModulus; + } + + /** + * Returns the key representation. + * + * @return the key representation. + */ + public KeyRepresentation getKeyRepresentation() { + return keyRepresentation; + } + + /** + * Returns the number of digits in the generated code. + * + * @return the number of digits in the generated code. + */ + public int getCodeDigits() { + return codeDigits; + } + + /** + * Returns the time step size, in milliseconds, as specified by RFC 6238. + * The default value is 30.000. + * + * @return the time step size in milliseconds. + */ + public long getTimeStepSizeInMillis() { + return timeStepSizeInMillis; + } + + /** + * Returns an integer value representing the number of windows of size + * timeStepSizeInMillis that are checked during the validation process, to + * account for differences between the server and the client clocks. The + * bigger the window, the more tolerant the library code is about clock + * skews. + *

+ * We are using Google's default behaviour of using a window size equal to + * 3. The limit on the maximum window size, present in older versions of + * this library, has been removed. + * + * @return the window size. + * @see #timeStepSizeInMillis + */ + public int getWindowSize() { + return windowSize; + } + + public static class GoogleAuthenticatorConfigBuilder { + private GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig(); + + public GoogleAuthenticatorConfig build() { + return config; + } + + public GoogleAuthenticatorConfigBuilder setCodeDigits(int codeDigits) { + if (codeDigits <= 0) { + throw new IllegalArgumentException("Code digits must be positive."); + } + + if (codeDigits < 6) { + throw new IllegalArgumentException("The minimum number of digits is 6."); + } + + if (codeDigits > 8) { + throw new IllegalArgumentException("The maximum number of digits is 8."); + } + + config.codeDigits = codeDigits; + config.keyModulus = (int) Math.pow(10, codeDigits); + return this; + } + + public GoogleAuthenticatorConfigBuilder setTimeStepSizeInMillis(long timeStepSizeInMillis) { + if (timeStepSizeInMillis <= 0) { + throw new IllegalArgumentException("Time step size must be positive."); + } + + config.timeStepSizeInMillis = timeStepSizeInMillis; + return this; + } + + public GoogleAuthenticatorConfigBuilder setWindowSize(int windowSize) { + if (windowSize <= 0) { + throw new IllegalArgumentException("Window number must be positive."); + } + + config.windowSize = windowSize; + return this; + } + + public GoogleAuthenticatorConfigBuilder setKeyRepresentation(KeyRepresentation keyRepresentation) { + if (keyRepresentation == null) { + throw new IllegalArgumentException("Key representation cannot be null."); + } + + config.keyRepresentation = keyRepresentation; + return this; + } + } +} diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java new file mode 100644 index 00000000..77a742b8 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +package com.sismics.util.totp; + +/** + * Date: 12/02/14 + * Time: 13:36 + * + * @author Enrico M. Crisostomo + */ +public class GoogleAuthenticatorException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Builds an exception with the provided error message. + * + * @param message the error message. + */ + public GoogleAuthenticatorException(String message) { + super(message); + } + + /** + * Builds an exception with the provided error mesasge and + * the provided cuase. + * + * @param message the error message. + * @param cause the cause. + */ + public GoogleAuthenticatorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java new file mode 100644 index 00000000..ee687153 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java @@ -0,0 +1,105 @@ +/* + * 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. + */ + +package com.sismics.util.totp; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is a JavaBean used by the GoogleAuthenticator library to represent + * a secret key. + *

+ * This class is immutable. + *

+ * Instance of this class should only be constructed by the GoogleAuthenticator + * library. + * + * @author Enrico M. Crisostomo + * @version 1.0 + * @see GoogleAuthenticator + * @since 1.0 + */ +public final class GoogleAuthenticatorKey { + /** + * The secret key in Base32 encoding. + */ + private final String key; + + /** + * The verification code at time = 0 (the UNIX epoch). + */ + private final int verificationCode; + + /** + * The list of scratch codes. + */ + private final List scratchCodes; + + /** + * The constructor with package visibility. + * + * @param secretKey the secret key in Base32 encoding. + * @param code the verification code at time = 0 (the UNIX epoch). + * @param scratchCodes the list of scratch codes. + */ + GoogleAuthenticatorKey(String secretKey, int code, List scratchCodes) { + key = secretKey; + verificationCode = code; + this.scratchCodes = new ArrayList<>(scratchCodes); + } + + /** + * Get the list of scratch codes. + * + * @return the list of scratch codes. + */ + public List getScratchCodes() { + return scratchCodes; + } + + /** + * Returns the secret key in Base32 encoding. + * + * @return the secret key in Base32 encoding. + */ + public String getKey() { + return key; + } + + /** + * Returns the verification code at time = 0 (the UNIX epoch). + * + * @return the verificationCode at time = 0 (the UNIX epoch). + */ + public int getVerificationCode() { + return verificationCode; + } +} diff --git a/docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java b/docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java new file mode 100644 index 00000000..9461ba08 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package com.sismics.util.totp; + +public enum KeyRepresentation { + BASE32, + BASE64 +} diff --git a/docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java b/docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java new file mode 100644 index 00000000..fd135d6f --- /dev/null +++ b/docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java @@ -0,0 +1,113 @@ +/* + * 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. + */ + +package com.sismics.util.totp; + +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Date: 08/04/14 Time: 15:21 + * + * @author Enrico M. Crisostomo + */ +class ReseedingSecureRandom { + private static final int MAX_OPERATIONS = 1_000_000; + private final String provider; + private final String algorithm; + private final AtomicInteger count = new AtomicInteger(0); + private SecureRandom secureRandom; + + ReseedingSecureRandom() { + this.algorithm = null; + this.provider = null; + + buildSecureRandom(); + } + + ReseedingSecureRandom(String algorithm) { + if (algorithm == null) { + throw new IllegalArgumentException("Algorithm cannot be null."); + } + + this.algorithm = algorithm; + this.provider = null; + + buildSecureRandom(); + } + + ReseedingSecureRandom(String algorithm, String provider) { + if (algorithm == null) { + throw new IllegalArgumentException("Algorithm cannot be null."); + } + + if (provider == null) { + throw new IllegalArgumentException("Provider cannot be null."); + } + + this.algorithm = algorithm; + this.provider = provider; + + buildSecureRandom(); + } + + private void buildSecureRandom() { + try { + if (this.algorithm == null && this.provider == null) { + this.secureRandom = new SecureRandom(); + } else if (this.provider == null) { + this.secureRandom = SecureRandom.getInstance(this.algorithm); + } else { + this.secureRandom = SecureRandom.getInstance(this.algorithm, this.provider); + } + } catch (NoSuchAlgorithmException e) { + throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified algorithm: %s. " + + "Another provider can be chosen setting the %s system property.", this.algorithm, GoogleAuthenticator.RNG_ALGORITHM), e); + } catch (NoSuchProviderException e) { + throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified provider: %s. " + + "Another provider can be chosen setting the %s system property.", this.provider, GoogleAuthenticator.RNG_ALGORITHM_PROVIDER), e); + } + } + + void nextBytes(byte[] bytes) { + if (count.incrementAndGet() > MAX_OPERATIONS) { + synchronized (this) { + if (count.get() > MAX_OPERATIONS) { + buildSecureRandom(); + count.set(0); + } + } + } + + this.secureRandom.nextBytes(bytes); + } +} diff --git a/docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java b/docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java new file mode 100644 index 00000000..37dddb00 --- /dev/null +++ b/docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java @@ -0,0 +1,26 @@ +package com.sismics.util; + +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +import com.sismics.util.totp.GoogleAuthenticator; +import com.sismics.util.totp.GoogleAuthenticatorKey; + +/** + * Test of {@link GoogleAuthenticator} + * + * @author bgamard + */ +public class TestGoogleAuthenticator { + @Test + public void testGoogleAuthenticator() { + GoogleAuthenticator gAuth = new GoogleAuthenticator(); + GoogleAuthenticatorKey key = gAuth.createCredentials(); + Assert.assertNotNull(key.getVerificationCode()); + Assert.assertEquals(5, key.getScratchCodes().size()); + int validationCode = gAuth.calculateCode(key.getKey(), new Date().getTime() / 30000); + Assert.assertTrue(gAuth.authorize(key.getKey(), validationCode)); + } +} diff --git a/docs-parent/pom.xml b/docs-parent/pom.xml index c86fa7e0..81362ea6 100644 --- a/docs-parent/pom.xml +++ b/docs-parent/pom.xml @@ -40,7 +40,6 @@ 3.2.1 1.6.5 1.3.1 - 0.6.0 9.2.13.v20150730 9.2.13.v20150730 @@ -410,13 +409,6 @@ ${com.github.jai-imageio.jai-imageio-core.version} - - - com.warrenstrange - googleauth - ${com.warrenstrange.googleauth} - - diff --git a/docs-web/pom.xml b/docs-web/pom.xml index 5b072a9c..048cc060 100644 --- a/docs-web/pom.xml +++ b/docs-web/pom.xml @@ -89,11 +89,6 @@ servlet - - com.warrenstrange - googleauth - - com.sismics.docs diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 960846ff..16cf7593 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -55,8 +55,8 @@ import com.sismics.rest.util.JsonUtil; import com.sismics.rest.util.ValidationUtil; import com.sismics.security.UserPrincipal; import com.sismics.util.filter.TokenBasedSecurityFilter; -import com.warrenstrange.googleauth.GoogleAuthenticator; -import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.sismics.util.totp.GoogleAuthenticator; +import com.sismics.util.totp.GoogleAuthenticatorKey; /** * User REST resources. @@ -649,7 +649,6 @@ public class UserResource extends BaseResource { } // Create a new TOTP key and scratch codes - // TODO Copy library sources here to scrap useless dependencies and make verification code generation public for testing GoogleAuthenticator gAuth = new GoogleAuthenticator(); final GoogleAuthenticatorKey key = gAuth.createCredentials();