package com.zy.common.utils;
|
|
import com.core.common.Cools;
|
|
import javax.crypto.Mac;
|
import javax.crypto.spec.SecretKeySpec;
|
import java.io.ByteArrayOutputStream;
|
import java.net.URLEncoder;
|
import java.nio.ByteBuffer;
|
import java.nio.charset.StandardCharsets;
|
import java.security.SecureRandom;
|
import java.util.Locale;
|
|
public final class MfaTotpUtil {
|
|
private static final char[] BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
private static final int SECRET_SIZE = 20;
|
private static final int OTP_DIGITS = 6;
|
private static final int OTP_PERIOD_SECONDS = 30;
|
|
private MfaTotpUtil() {
|
}
|
|
public static String generateSecret() {
|
byte[] buffer = new byte[SECRET_SIZE];
|
SECURE_RANDOM.nextBytes(buffer);
|
return encodeBase32(buffer);
|
}
|
|
public static boolean verifyCode(String secret, String code, int window) {
|
if (Cools.isEmpty(secret, code)) {
|
return false;
|
}
|
String normalizedCode = String.valueOf(code).replaceAll("\\s+", "");
|
if (!normalizedCode.matches("\\d{" + OTP_DIGITS + "}")) {
|
return false;
|
}
|
try {
|
long currentStep = System.currentTimeMillis() / 1000L / OTP_PERIOD_SECONDS;
|
for (int offset = -window; offset <= window; offset++) {
|
if (normalizedCode.equals(generateCode(secret, currentStep + offset))) {
|
return true;
|
}
|
}
|
} catch (Exception ignored) {
|
}
|
return false;
|
}
|
|
public static String buildOtpAuthUri(String issuer, String account, String secret) {
|
String safeIssuer = Cools.isEmpty(issuer) ? "WCS" : issuer.trim();
|
String safeAccount = Cools.isEmpty(account) ? "user" : account.trim();
|
String label = urlEncode(safeIssuer + ":" + safeAccount);
|
return "otpauth://totp/" + label
|
+ "?secret=" + secret
|
+ "&issuer=" + urlEncode(safeIssuer)
|
+ "&algorithm=SHA1&digits=" + OTP_DIGITS
|
+ "&period=" + OTP_PERIOD_SECONDS;
|
}
|
|
public static String maskSecret(String secret) {
|
if (Cools.isEmpty(secret)) {
|
return "";
|
}
|
String value = String.valueOf(secret).trim();
|
if (value.length() <= 8) {
|
return value;
|
}
|
return value.substring(0, 4) + "****" + value.substring(value.length() - 4);
|
}
|
|
private static String generateCode(String secret, long step) {
|
try {
|
byte[] key = decodeBase32(secret);
|
byte[] data = ByteBuffer.allocate(8).putLong(step).array();
|
Mac mac = Mac.getInstance("HmacSHA1");
|
mac.init(new SecretKeySpec(key, "HmacSHA1"));
|
byte[] hash = mac.doFinal(data);
|
int offset = hash[hash.length - 1] & 0x0F;
|
int binary = ((hash[offset] & 0x7F) << 24)
|
| ((hash[offset + 1] & 0xFF) << 16)
|
| ((hash[offset + 2] & 0xFF) << 8)
|
| (hash[offset + 3] & 0xFF);
|
int otp = binary % (int) Math.pow(10, OTP_DIGITS);
|
return String.format(Locale.ROOT, "%0" + OTP_DIGITS + "d", otp);
|
} catch (Exception e) {
|
throw new IllegalStateException("generate totp code failed", e);
|
}
|
}
|
|
private static String encodeBase32(byte[] data) {
|
StringBuilder builder = new StringBuilder((data.length * 8 + 4) / 5);
|
int buffer = 0;
|
int bitsLeft = 0;
|
for (byte datum : data) {
|
buffer = (buffer << 8) | (datum & 0xFF);
|
bitsLeft += 8;
|
while (bitsLeft >= 5) {
|
builder.append(BASE32_ALPHABET[(buffer >> (bitsLeft - 5)) & 0x1F]);
|
bitsLeft -= 5;
|
}
|
}
|
if (bitsLeft > 0) {
|
builder.append(BASE32_ALPHABET[(buffer << (5 - bitsLeft)) & 0x1F]);
|
}
|
return builder.toString();
|
}
|
|
private static byte[] decodeBase32(String value) {
|
String normalized = String.valueOf(value)
|
.trim()
|
.replace("=", "")
|
.replace(" ", "")
|
.replace("-", "")
|
.toUpperCase(Locale.ROOT);
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
int buffer = 0;
|
int bitsLeft = 0;
|
for (int i = 0; i < normalized.length(); i++) {
|
char current = normalized.charAt(i);
|
int index = indexOfBase32(current);
|
if (index < 0) {
|
throw new IllegalArgumentException("invalid base32 secret");
|
}
|
buffer = (buffer << 5) | index;
|
bitsLeft += 5;
|
if (bitsLeft >= 8) {
|
output.write((buffer >> (bitsLeft - 8)) & 0xFF);
|
bitsLeft -= 8;
|
}
|
}
|
return output.toByteArray();
|
}
|
|
private static int indexOfBase32(char value) {
|
for (int i = 0; i < BASE32_ALPHABET.length; i++) {
|
if (BASE32_ALPHABET[i] == value) {
|
return i;
|
}
|
}
|
return -1;
|
}
|
|
private static String urlEncode(String value) {
|
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
|
}
|
}
|