package io.mangoo.utils; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Objects; import java.util.Random; import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Base32; import org.apache.commons.lang3.RegExUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import io.mangoo.crypto.totp.TOTP; import io.mangoo.enums.HmacShaAlgorithm; import io.mangoo.enums.Required; /** * * @author svenkubiak * */ public class TotpUtils { private static final Logger LOG = LogManager.getLogger(TotpUtils.class); private static final Base32 base32 = new Base32(); private static final HmacShaAlgorithm ALGORITHM = HmacShaAlgorithm.HMAC_SHA_512; private static final int DIGITS = 6; private static final int MAX_CHARACTERS = 32; private static final int PERIOD = 30; private static final int ITERATIONS = 26; private static final int BYTES_SECRET = 64; private TotpUtils() { } /** * Generates a 64 byte (512 bit) secret * * @return A 64 characters random string based on SecureRandom */ public static String createSecret() { Random random = new SecureRandom(); StringBuilder buffer = new StringBuilder(BYTES_SECRET); for (int i = 0; i < BYTES_SECRET; i++) { int value = random.nextInt(MAX_CHARACTERS); if (value < ITERATIONS) { buffer.append((char) ('A' + value)); } else { buffer.append((char) ('2' + (value - ITERATIONS))); } } return buffer.toString(); } /** * Creates the current TOTP based on the following default values: * SHA512 algorithm, 6 digits, 30 seconds time period * * @param secret The secret to use * * @return The totp value or null if generation failed */ public static String getTotp(String secret) { Objects.requireNonNull(secret, Required.SECRET.toString()); String value = null; try { TOTP builder = TOTP.key(secret.getBytes(StandardCharsets.US_ASCII.name())) .timeStep(TimeUnit.SECONDS.toMillis(PERIOD)) .digits(DIGITS) .hmacSha(ALGORITHM) .build(); value = builder.value(); } catch (UnsupportedEncodingException e) { LOG.error("Failed to create TOTP", e); } return value; } /** * Creates the current TOTP based on the given parameters * * @param secret The secret to use * @param algorithm The algorithm to use * @param digits The digits to use (6 or 8) * @param period The time period in seconds * * @return The totp value or null if generation failed */ public static String getTotp(String secret, HmacShaAlgorithm algorithm, int digits, int period) { Objects.requireNonNull(secret, Required.SECRET.toString()); Objects.requireNonNull(algorithm, Required.ALGORITHM.toString()); String value = null; try { TOTP builder = TOTP.key(secret.getBytes(StandardCharsets.US_ASCII.name())) .timeStep(TimeUnit.SECONDS.toMillis(period)) .digits(digits) .hmacSha(algorithm) .build(); value = builder.value(); } catch (UnsupportedEncodingException e) { LOG.error("Failed to create TOTP", e); } return value; } /** * Verifies a given TOTP based on the following default values: * SHA512 algorithm, 6 digits, 30 seconds time period * * @param secret The secret to use * @param totp The TOTP to verify * * @return True if the TOTP is valid, false otherwise */ public static boolean verifiedTotp(String secret, String totp) { Objects.requireNonNull(secret, Required.SECRET.toString()); Objects.requireNonNull(totp, Required.TOTP.toString()); String value = null; try { TOTP builder = TOTP.key(secret.getBytes(StandardCharsets.US_ASCII.name())) .timeStep(TimeUnit.SECONDS.toMillis(PERIOD)) .digits(DIGITS) .hmacSha(ALGORITHM) .build(); value = builder.value(); } catch (UnsupportedEncodingException e) { LOG.error("Failed to verify TOTP", e); } return totp.equals(value); } /** * Generates a QR code link from google charts API to share a secret with a user * * @param name The name of the account * @param issuer The name of the issuer * @param secret The secret to use * @param algorithm The algorithm to use * @param digits The number of digits to use * @param period The period to use * * @return An URL to Google charts API with the QR code */ public static String getQRCode(String name, String issuer, String secret, HmacShaAlgorithm algorithm, String digits, String period) { Objects.requireNonNull(name, Required.ACCOUNT_NAME.toString()); Objects.requireNonNull(secret, Required.SECRET.toString()); Objects.requireNonNull(issuer, Required.ISSUER.toString()); Objects.requireNonNull(algorithm, Required.ALGORITHM.toString()); Objects.requireNonNull(digits, Required.DIGITS.toString()); Objects.requireNonNull(period, Required.PERIOD.toString()); var buffer = new StringBuilder(); buffer.append("https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=200x200&chld=M|0&cht=qr&chl=") .append(getOtpauthURL(name, issuer, secret, algorithm, digits, period)); return buffer.toString(); } /** * Generates a otpauth code to share a secret with a user * * @param name The name of the account * @param issuer The name of the issuer * @param secret The secret to use * @param algorithm The algorithm to use * @param digits The number of digits to use * @param period The period to use * * @return An otpauth url */ public static String getOtpauthURL(String name, String issuer, String secret, HmacShaAlgorithm algorithm, String digits, String period) { Objects.requireNonNull(name, Required.ACCOUNT_NAME.toString()); Objects.requireNonNull(secret, Required.SECRET.toString()); Objects.requireNonNull(issuer, Required.ISSUER.toString()); Objects.requireNonNull(algorithm, Required.ALGORITHM.toString()); Objects.requireNonNull(digits, Required.DIGITS.toString()); Objects.requireNonNull(period, Required.PERIOD.toString()); var buffer = new StringBuilder(); buffer.append("otpauth://totp/") .append(name) .append("?secret=") .append(RegExUtils.replaceAll(base32.encodeAsString(secret.getBytes(StandardCharsets.UTF_8)), "=", "")) .append("&algorithm=") .append(algorithm.getAlgorithm()) .append("&issuer=") .append(issuer) .append("&digits=") .append(digits) .append("&period=") .append(period); String url = ""; try { url = URLEncoder.encode(buffer.toString(), StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { LOG.error("Failed to encode otpauth url", e); } return url; } }