 
                        
        
                        
        rs.otp.TotpGen Maven / Gradle / Ivy
/**
 * 
 */
package rs.otp;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.crypto.URIReferenceException;
import rs.otp.secret.Base32Secret;
import rs.otp.secret.ISecret;
/**
 * Implementation of the Time-based One-Time Password (TOTP) two factor authentication algorithm. You need to:
 * 
 * 
 * - Use generateBase32Secret() to generate a secret key for a user.*
- Store the secret key in the database associated with the user account.*
- Display the QR image URL returned by qrImageUrl(...) to the user.*
- User uses the image to load the secret key into his authenticator application.*
* 
 *
 * Whenever the user logs in:
 * 
 * 
 * 
 * - The user enters the number from the authenticator application into the login form.*
- Read the secret associated with the user account from the database.*
- The server compares the user input with the output from generateCurrentNumber(...).*
- If they are equal then the user is allowed to log in.*
* 
 *
 * The original class was taken from two-factor-auth.
 * 
 * 
 * 
 * For more details about this magic algorithm, see: http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
 * 
 * 
 * @author graywatson
 * @author ralph
 */
public class TotpGen {
	/** default number of digits in a OTP string, 6 is default */
	public static final int DEFAULT_OTP_LENGTH = 6;
	/** default time-step which is part of the spec, 30 seconds is default */
	public static final int DEFAULT_TIME_STEP_SECONDS = 30;
	/** set to the number of digits to control 0 prefix, set to 0 for no prefix */
	private static final int MAX_NUM_DIGITS_OUTPUT = 100;
	private static final String blockOfZeros;
	static {
		char[] chars = new char[MAX_NUM_DIGITS_OUTPUT];
		Arrays.fill(chars, '0');
		blockOfZeros = new String(chars);
	}
	private ISecret secret;
	private int     numDigits;
	private int     timeStepSeconds;
	private String  issuer;
	private String  account;
	
	/**
	 * Creates the generator based on secret with 6 digits for the OTP and 30sec time step.
	 * @param secret - the secret
	 */
	public TotpGen(ISecret secret) {
		this(secret, DEFAULT_OTP_LENGTH, DEFAULT_TIME_STEP_SECONDS);
	}
	/**
	 * Creates the generator based on secret with the digits for the OTP.
	 * @param secret - the secret
	 * @param numDigits - the number of digits to produce
	 */
	public TotpGen(ISecret secret, int numDigits) {
		this(secret, numDigits, DEFAULT_TIME_STEP_SECONDS);
	}
	/**
	 * Creates the generator based on secret with the digits for the OTP.
	 * @param secret - the secret
	 * @param numDigits - the number of digits to produce
	 * @param timeStepSeconds - the time step in seconds
	 */
	public TotpGen(ISecret secret, int numDigits, int timeStepSeconds) {
		this.secret          = secret;
		this.numDigits       = numDigits;
		this.timeStepSeconds = timeStepSeconds;
	}
	/**
	 * Returns the secret.
	 * @return the secret
	 */
	public ISecret getSecret() {
		return secret;
	}
	/**
	 * Returns the number of digits to produce.
	 * @return the number of digits to produce
	 */
	public int getNumDigits() {
		return numDigits;
	}
	/**
	 * Returns the time step in seconds.
	 * @return the time step in seconds
	 */
	public int getTimeStepSeconds() {
		return timeStepSeconds;
	}
	/**
	 * Returns the issuer of this TOTP (info only). Can be {@code null} but must not contain colons.
	 * @return the issuer
	 */
	public String getIssuer() {
		return issuer;
	}
	/**
	 * Sets the issuer of this TOTP (info only).
	 * @param issuer the issuer of this TOTP (info only). Can be {@code null} but must not contain colons.
	 */
	public void setIssuer(String issuer) {
		this.issuer = issuer;
	}
	/**
	 * Sets the account of this TOTP (info only). Can be {@code null}.
	 * @return the account
	 */
	public String getAccount() {
		return account;
	}
	/**
	 * Sets the account of this TOTP (info only).
	 * @param account the account of this TOTP.  Can be {@code null}.
	 */
	public void setAccount(String account) {
		this.account = account;
	}
	/**
	 * Returns the otpauth URI scheme to be used e.g. for QR codes.
	 * Uses the issuer and account strings of this generator, if set.
	 * Please refer to otpauth URI scheme.
	 * @return the URI to be used when adding to external auth generators.
	 */
	public URI getUri() {
		return getUri(issuer, account);
	}
	/**
	 * Returns the otpauth URI scheme to be used e.g. for QR codes.
	 * Uses the issuer string of this generator, if set.
	 * Please refer to otpauth URI scheme.
	 * @param account - name of account
	 * @return the URI to be used when adding to external auth generators.
	 */
	public URI getUri(String account) {
		return getUri(issuer, account);
	}
	/**
	 * Returns the otpauth URI scheme to be used e.g. for QR codes.
	 * Please refer to otpauth URI scheme.
	 * @param issuer - issuer of the key, may be {@code null} but must not contain colon
	 * @param account - name of account
	 * @return the URI to be used when adding to external auth generators.
	 */
	public URI getUri(String issuer, String account) {
		if (account == null) account = this.account;
		if (account == null) throw new RuntimeException("Cannot use empty account string");
		StringBuilder rc = new StringBuilder();
		rc.append("otpauth://totp/");
		if (issuer != null) {
			rc.append(URLEncoder.encode(issuer, StandardCharsets.UTF_8));
			rc.append(":");
		}
		rc.append(account);
		rc.append("?secret=");
		rc.append(secret.encode());
		rc.append("&digits=");
		rc.append(numDigits);
		if (timeStepSeconds != 30) {
			rc.append("&period=");
			rc.append(timeStepSeconds);
		}
		if (issuer != null) {
			rc.append("&issuer=");
			rc.append(URLEncoder.encode(issuer, StandardCharsets.UTF_8));
		}
		try {
			return new URI(rc.toString());
		} catch (URISyntaxException e) {
			throw new RuntimeException("Cannot create URI: ", e);
		}
	}
	/**
	 * Validates an OTP. 
	 * 
	 * WARNING: This requires a system clock that is in sync with the world.
	 * 
	 * @param otp
	 *            One time password provided by the user from their authenticator application.
	 * @return True if the OTP matches the calculated OTP at exactly this moment.
	 * @throws GeneralSecurityException when the verification cannot be performed
	 */
	public boolean verify(String otp) throws GeneralSecurityException {
		return verify(otp, 0L, System.currentTimeMillis(), timeStepSeconds, numDigits);
	}
	/**
	 * Validates an OTP. 
	 * This allows you to set a window in milliseconds to account for people being close to the end of the time-step. 
	 * For example, if windowMillis is 10000 then this method will check the OTP against the generated number from 
	 * 10 seconds before now through 10 seconds after now.
	 * 
	 * WARNING: This requires a system clock that is in sync with the world.
	 * 
	 * @param otp
	 *            One time password provided by the user from their authenticator application.
	 * @param windowMillis
	 *            Number of milliseconds that they are allowed to be off and still match. This checks before and after
	 *            the current time to account for clock variance. Set to 0 for no window.
	 * @return True if the OTP matched the calculated OTP within the specified window.
	 * @throws GeneralSecurityException when the verification cannot be performed
	 */
	public boolean verify(String otp, long windowMillis) throws GeneralSecurityException {
		return verify(otp, windowMillis, System.currentTimeMillis(), timeStepSeconds, numDigits);
	}
	/**
	 * Validates an OTP. 
	 * This allows you to set a window in milliseconds to account for people being close to the end of the time-step. 
	 * For example, if windowMillis is 10000 then this method will check the OTP against the generated number from 
	 * 10 seconds before now through 10 seconds after now.
	 * 
	 * @param otp
	 *            One time password provided by the user from their authenticator application.
	 * @param windowMillis
	 *            Number of milliseconds that they are allowed to be off and still match. This checks before and after
	 *            the current time to account for clock variance. Set to 0 for no window.
	 * @param timeInMillis
	 *            Time in milliseconds.
	 * @param timeStepSeconds
	 *            Time step in seconds. The default value is 30 seconds here. See {@link #DEFAULT_TIME_STEP_SECONDS}.
	 * @param numDigits
	 *            The number of digits of the OTP.
	 * @return True if the OTP matched the calculated OTP within the specified window.
	 * @throws GeneralSecurityException when the verification cannot be performed
	 */
	protected boolean verify(String otp, long windowMillis, long timeInMillis, int timeStepSeconds, int numDigits) throws GeneralSecurityException {
		return verifyOtp(otp, windowMillis, timeInMillis, timeStepSeconds, numDigits);
	}
	/**
	 * Returns the current OTP. 
	 * 
	 * WARNING: This requires a system clock that is in sync with the world.
	 * 
	 * @return The OTP which should match the user's authenticator application output.
	 * @throws GeneralSecurityException when the generation cannot be performed
	 */
	public String current() throws GeneralSecurityException {
		return otpAt(System.currentTimeMillis(), timeStepSeconds, numDigits);
	}
	
	/**
	 * Returns an OTP at a given time. 
	 * 
	 * @param timeInMillis
	 *            Time in milliseconds.
	 * @param timeStepSeconds
	 *            Time step in seconds. The default value is 30 seconds here. See {@link #DEFAULT_TIME_STEP_SECONDS}.
	 * @param numDigits
	 *            The number of digits of the OTP.
	 * @return The OTP which should match the user's authenticator application output.
	 * @throws GeneralSecurityException when the generation cannot be performed
	 */
	protected String otpAt(long timeInMillis, int timeStepSeconds, int numDigits) throws GeneralSecurityException {
		long timeIndex = getTimeIndex(timeInMillis, timeStepSeconds);
		return stringify(currentOtp(timeIndex, numDigits), numDigits);
	}
	/**
	 * Returns the OTP's time index based on the given time and time step duration.
	 * @param timeInMillis - time in ms
	 * @param timeStepSeconds - time step duration
	 * @return the time index
	 */
	private long getTimeIndex(long timeInMillis, int timeStepSeconds) {
		return timeInMillis / 1000 / timeStepSeconds;
	}
	/**
	 * Internal helper method actually calculating the current OTP.
	 * @param timeIndex - the time index to calculate
	 * @param numDigits - the number of digits to be used
	 * @return the OTP as a number (needs to be 0-padded on the left for string representation)
	 * @throws GeneralSecurityException when the generation fails
	 */
	private int currentOtp(long timeIndex, int numDigits) throws GeneralSecurityException {
		byte[] data = new byte[8];
		for (int i = 7; timeIndex > 0; i--) {
			data[i] = (byte) (timeIndex & 0xFF);
			timeIndex >>= 8;
		}
		// encrypt the data with the key and return the SHA1 of it in hex
		SecretKeySpec signKey = new SecretKeySpec(secret.getBytes(), "HmacSHA1");
		// if this is expensive, could put in a thread-local
		Mac mac = Mac.getInstance("HmacSHA1");
		mac.init(signKey);
		byte[] hash = mac.doFinal(data);
		// take the 4 least significant bits from the encrypted string as an offset
		int offset = hash[hash.length - 1] & 0xF;
		// We're using a long because Java hasn't got unsigned int.
		long truncatedHash = 0;
		for (int i = offset; i < offset + 4; ++i) {
			truncatedHash <<= 8;
			// get the 4 bytes at the offset
			truncatedHash |= (hash[i] & 0xFF);
		}
		// cut off the top bit
		truncatedHash &= 0x7FFFFFFF;
		// the token is then the last  digits in the number
		long mask = 1;
		for (int i = 0; i < numDigits; i++) {
			mask *= 10;
		}
		truncatedHash %= mask;
		return (int) truncatedHash;
	}
	/**
	 * Internal helper method to verify an OTP given.
	 * @param otp - the OTP as a string
	 * @param windowMillis
	 *            Number of milliseconds that they are allowed to be off and still match. This checks before and after
	 *            the current time to account for clock variance. Set to 0 for no window.
	 * @param timeInMillis
	 *            Time in milliseconds.
	 * @param timeStepSeconds
	 *            Time step in seconds. The default value is 30 seconds here. See {@link #DEFAULT_TIME_STEP_SECONDS}.
	 * @param numDigits
	 *            The number of digits of the OTP.
	 * @return whether the OTP verifies successfully
	 * @throws GeneralSecurityException when the verification cannot be performed
	 */
	protected boolean verifyOtp(String otp, long windowMillis, long timeInMillis, int timeStepSeconds, int numDigits) throws GeneralSecurityException {
		try {
			return verifyOtp(Integer.parseInt(otp), windowMillis, timeInMillis, timeStepSeconds, numDigits);
		} catch (NumberFormatException e) {
			throw new GeneralSecurityException("OTP is not a valid number: "+otp);
		}
	}
	/**
	 * Internal helper method to verify an OTP given.
	 * @param otp - the OTP as number
	 * @param windowMillis
	 *            Number of milliseconds that they are allowed to be off and still match. This checks before and after
	 *            the current time to account for clock variance. Set to 0 for no window.
	 * @param timeInMillis
	 *            Time in milliseconds.
	 * @param timeStepSeconds
	 *            Time step in seconds. The default value is 30 seconds here. See {@link #DEFAULT_TIME_STEP_SECONDS}.
	 * @param numDigits
	 *            The number of digits of the OTP.
	 * @return whether the OTP verifies successfully
	 * @throws GeneralSecurityException when the verification cannot be performed
	 */
	private boolean verifyOtp(int otp, long windowMillis, long timeInMillis, int timeStepSeconds, int numDigits) throws GeneralSecurityException {
		if (windowMillis <= 0) {
			// just test the current time
			long timeIndex = getTimeIndex(timeInMillis, timeStepSeconds);
			long current   = currentOtp(timeIndex, numDigits);
			return (current == otp);
		}
		// maybe check multiple values
		long startIndex = getTimeIndex(timeInMillis - windowMillis, timeStepSeconds);
		long endIndex = getTimeIndex(timeInMillis + windowMillis, timeStepSeconds);
		for (long timeIndex = startIndex; timeIndex <= endIndex; timeIndex++) {
			long current = currentOtp(timeIndex, numDigits);
			if (current == otp) {
				return true;
			}
		}
		return false;
	}
	/**
	 * Return the string prepended with 0s.
	 * @param otp - the OTP to pad
	 * @param digits - the digits to be achieved
	 * @return the 0-padded OTP
	 */
	protected static String stringify(int otp, int digits) {
		String numStr = Integer.toString(otp);
		if (numStr.length() >= digits) {
			return numStr;
		} else {
			StringBuilder sb = new StringBuilder(digits);
			int zeroCount = digits - numStr.length();
			sb.append(blockOfZeros, 0, zeroCount);
			sb.append(numStr);
			return sb.toString();
		}
	}
	/**
	 * Creates a generator from the given URI.
	 * Please refer to otpauth URI scheme.
	 * @param uri - the TOTP URI
	 * @return the generator from this URI
	 * @throws URISyntaxException when the URI is syntactically invalid
	 * @throws URIReferenceException when the URI is semantically invalid
	 */
	public static TotpGen from(String uri) throws URISyntaxException, URIReferenceException {
		return from(new URI(uri));
	}
	
	/**
	 * Creates a generator from the given URI.
	 * Please refer to otpauth URI scheme.
	 * @param uri - the TOTP URI
	 * @return the generator from this URI
	 * @throws URIReferenceException when the URI is semantically invalid
	 */
	public static TotpGen from(URI uri) throws URIReferenceException {
		if (!"otpauth".equalsIgnoreCase(uri.getScheme())) {
			throw new URIReferenceException("Not a otpauth:// URI.");
		}
		if (!"totp".equalsIgnoreCase(uri.getHost())) {
			throw new URIReferenceException("Not a TOTP URI.");
		}
		String path = uri.getPath().substring(1);
		String parts[] = path.split(":");
		String issuer  = parts.length > 1 ? parts[0] : null;
		String account = parts.length > 1 ? parts[1] : parts[0];
		String query   = uri.getQuery();
		Map params = new HashMap<>();
		try {
			for (String part : query.split("&")) {
				String kv[] = part.split("=");
				params.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8), URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
			}
		} catch (Throwable t) {
			throw new URIReferenceException("No valid TOTP parameters");
		}
		if (!params.containsKey("secret")) {
			throw new URIReferenceException("No valid TOTP parameters: secret missing");
		}
		ISecret secret = new Base32Secret(params.get("secret"));
		int     digits = 6;  
		if (params.containsKey("digits")) {
			digits = Integer.parseInt(params.get("digits"));
		}
		if (params.containsKey("issuer")) {
			issuer = params.get("issuer");
		}
		int period = 30;
		if (params.containsKey("period")) {
			period = Integer.parseInt(params.get("period"));
		}
		TotpGen rc = new TotpGen(secret, digits, period);
		rc.setAccount(account);
		rc.setIssuer(issuer);
		return rc;
	}
	
	public static void main(String args[]) {
		if (args.length > 0) {
			try {
				String testSecret = args[0];
				TotpGen utils = new TotpGen(new Base32Secret(testSecret));
				String currentOtp = null;
				while (true) {
					String otp = utils.current();
					if (!otp.equalsIgnoreCase(currentOtp)) {
						System.out.println(otp);
						currentOtp = otp;
					}
					Thread.sleep(1000L);
				}
			} catch (Throwable t) {
				t.printStackTrace();
			}
		} else {
			System.out.println("You need to give the Base32 secret as argument.");
		}
	}
}
  © 2015 - 2025 Weber Informatics LLC | Privacy Policy