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