package io.mangoo.helpers; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Objects; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base32; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import io.mangoo.enums.Required; /** * Two factor Java implementation for the Time-based One-Time Password (TOTP) algorithm. * * See: https://github.com/j256/java-two-factor-auth * * Copyright 2015, Gray Watson * * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby * granted provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS * PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, * OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS * SOFTWARE. * * @author graywatson, svenkubiak, WilliamDunne */ @SuppressWarnings("all") public class TwoFactorHelper { private final Logger LOG = LogManager.getLogger(TwoFactorHelper.class); private final Base32 base32 = new Base32(); private final String HMAC_SHA1 = "HmacSHA1"; private final String BLOCK_OF_ZEROS = "000000"; private final int TIME_STEP_SECONDS = 30; private final boolean USE_SHA1_THREAD_LOCAL = true; /** * Validate a given code using the secret, defaults to window of 3 either side, * allowing a margin of error equivalent to three windows to adjust for time * discrepancies. * * Uses the current time. * * @param number code provided by user * @param secret the secret for the users code * * @return boolean if the code is valid */ public boolean validateCurrentNumber(int number, String secret) { return validateCurrentNumber(number, secret, 3); } /** * Validate a given code at a specific time, and specific window * * @param number the code provided by the user. * @param secret the secret used to generate the users code * @param window the number of windows to check around the time * @param time the time in milliseconds at which the code should be checked * * @return True if the code is valid within the timeframe, false otherwise */ public boolean validateCurrentNumber(int number, String secret, int window, long time) { try { int current = Integer.parseInt(generateCurrentNumber(secret, time)); if (number == current) { return true; } else if (validateCurrentNumberLow(number, secret, window - 1, time - TIME_STEP_SECONDS * 1000)) { return true; } else if (validateCurrentNumberHigh(number, secret, window - 1, time + TIME_STEP_SECONDS * 1000)) { return true; } } catch(GeneralSecurityException e) { LOG.error("Failed to validate number", e); } return false; } /** * Validate a given code using the secret, provided number, and number of windows * to check. Uses currentTimeMillis for time * * @param number the code provided by the user * @param secret the secret for the users code * @param window the number of windows to check around the time * * @return True if the code is correct, false otherwise */ public boolean validateCurrentNumber(int number, String secret, int window) { long time = System.currentTimeMillis(); return validateCurrentNumber(number, secret, window, time); } private boolean validateCurrentNumberLow(int number, String secret, int window, Long time) throws GeneralSecurityException { int current = Integer.parseInt(generateCurrentNumber(secret, time)); if (current == number) { return true; } else { if (window > 0) { return validateCurrentNumberLow(number, secret, window - 1, time - TIME_STEP_SECONDS * 1000); } } return false; } private boolean validateCurrentNumberHigh(int number, String secret, int window, long time) throws GeneralSecurityException { int current = Integer.parseInt(generateCurrentNumber(secret, time)); if (current == number) { return true; } else { if (window > 0) { return validateCurrentNumberHigh(number, secret, window - 1, time + TIME_STEP_SECONDS * 1000); } } return false; } /** * Return the current number to be checked against the user input, using the * time found in System.currentTimeMillis() * * WARNING: This requires a system clock that is in sync with the world. * * For more details of this magic algorithm, see: * http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm * * @param secret The secret to use * * @return The current number to be checked */ public String generateCurrentNumber(String secret) { Objects.requireNonNull(secret, Required.SECRET.toString()); return generateCurrentNumber(secret, System.currentTimeMillis()); } /** * Same as {@link #generateCurrentNumber(String)} except at a particular time in milliseconds * * @param secret The secret to use * @param currentTimeMillis A provided time in milli seconds * * @return The current number to be checked */ public String generateCurrentNumber(String secret, long currentTimeMillis) { Objects.requireNonNull(secret, Required.GROUP_NAME.toString()); final byte[] key = secret.getBytes(); final byte[] data = new byte[8]; long value = currentTimeMillis / 1000 / TIME_STEP_SECONDS; for (int i = 7; value > 0; i--) { data[i] = (byte) (value & 0xFF); value >>= 8; } SecretKeySpec signKey = new SecretKeySpec(key, HMAC_SHA1); Mac mac = null; try { mac = Mac.getInstance(HMAC_SHA1); mac.init(signKey); } catch (NoSuchAlgorithmException | InvalidKeyException e) { LOG.error("Failed to get instance for HMAC SHA1", e); } long truncatedHash = 0; if (mac != null) { byte[] hash = mac.doFinal(data); int offset = hash[hash.length - 1] & 0xF; for (int i = offset; i < offset + 4; ++i) { truncatedHash <<= 8; truncatedHash |= (hash[i] & 0xFF); } truncatedHash &= 0x7FFFFFFF; truncatedHash %= 1000000; } return String.format("%06d", truncatedHash); } /** * Return the QR image URL from Google Charts API. * * This can be shown to the user and scanned by the authenticator program as an easy way to enter the secret * * @param accountName The account name used to display to the user * @param secret The plaintext secret to use * * @return A URL to the Google charts API */ public String generateQRCode(String accountName, String secret) { Objects.requireNonNull(accountName, Required.ACCOUNT_NAME.toString()); Objects.requireNonNull(secret, Required.SECRET.toString()); final StringBuilder buffer = new StringBuilder(128); buffer.append("https://chart.googleapis.com/chart") .append("?chs=200x200&cht=qr&chl=200x200&chld=M|0&cht=qr&chl=") .append("otpauth://totp/") .append(accountName) .append("?secret=") .append(base32.encodeAsString(secret.getBytes())); return buffer.toString(); } }