From bd6b518aae61608ddc2d82b43ccc283dc95b9c54 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期三, 11 三月 2026 13:59:33 +0800
Subject: [PATCH] #
---
src/main/java/com/zy/common/utils/MfaTotpUtil.java | 148 +++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 148 insertions(+), 0 deletions(-)
diff --git a/src/main/java/com/zy/common/utils/MfaTotpUtil.java b/src/main/java/com/zy/common/utils/MfaTotpUtil.java
new file mode 100644
index 0000000..01282a3
--- /dev/null
+++ b/src/main/java/com/zy/common/utils/MfaTotpUtil.java
@@ -0,0 +1,148 @@
+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");
+ }
+}
--
Gitblit v1.9.1