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/PasskeyWebAuthnUtil.java |  268 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 268 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/zy/common/utils/PasskeyWebAuthnUtil.java b/src/main/java/com/zy/common/utils/PasskeyWebAuthnUtil.java
new file mode 100644
index 0000000..80476a2
--- /dev/null
+++ b/src/main/java/com/zy/common/utils/PasskeyWebAuthnUtil.java
@@ -0,0 +1,268 @@
+package com.zy.common.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.core.common.Cools;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+
+public final class PasskeyWebAuthnUtil {
+
+    private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();
+
+    private PasskeyWebAuthnUtil() {
+    }
+
+    public static JSONObject parseClientData(String clientDataJsonBase64Url) {
+        if (Cools.isEmpty(clientDataJsonBase64Url)) {
+            throw new IllegalArgumentException("Missing clientDataJSON");
+        }
+        String json = new String(decodeBase64Url(clientDataJsonBase64Url), StandardCharsets.UTF_8);
+        JSONObject clientData = JSON.parseObject(json);
+        if (clientData == null) {
+            throw new IllegalArgumentException("Invalid clientDataJSON");
+        }
+        return clientData;
+    }
+
+    public static void validateClientData(JSONObject clientData, String expectedType, String expectedChallenge, String expectedOrigin) {
+        if (clientData == null) {
+            throw new IllegalArgumentException("Missing clientData");
+        }
+        if (!Cools.eq(expectedType, clientData.getString("type"))) {
+            throw new IllegalArgumentException("Unexpected WebAuthn type");
+        }
+        if (!Cools.eq(expectedChallenge, clientData.getString("challenge"))) {
+            throw new IllegalArgumentException("Challenge mismatch");
+        }
+        if (!Cools.eq(expectedOrigin, clientData.getString("origin"))) {
+            throw new IllegalArgumentException("Origin mismatch");
+        }
+    }
+
+    public static AuthenticatorData validateAuthenticatorData(String authenticatorDataBase64Url, String rpId, boolean requireUserVerification) throws GeneralSecurityException {
+        byte[] authenticatorData = decodeBase64Url(authenticatorDataBase64Url);
+        if (authenticatorData.length < 37) {
+            throw new GeneralSecurityException("Invalid authenticator data");
+        }
+        byte[] expectedRpIdHash = sha256(rpId.getBytes(StandardCharsets.UTF_8));
+        for (int i = 0; i < expectedRpIdHash.length; i++) {
+            if (authenticatorData[i] != expectedRpIdHash[i]) {
+                throw new GeneralSecurityException("RP ID hash mismatch");
+            }
+        }
+        int flags = authenticatorData[32] & 0xFF;
+        if ((flags & 0x01) == 0) {
+            throw new GeneralSecurityException("User presence required");
+        }
+        if (requireUserVerification && (flags & 0x04) == 0) {
+            throw new GeneralSecurityException("User verification required");
+        }
+        long signCount = ByteBuffer.wrap(authenticatorData, 33, 4).getInt() & 0xFFFFFFFFL;
+        return new AuthenticatorData(authenticatorData, flags, signCount);
+    }
+
+    public static void verifyAssertionSignature(String publicKeyBase64Url,
+                                                Integer algorithm,
+                                                String authenticatorDataBase64Url,
+                                                String clientDataJsonBase64Url,
+                                                String signatureBase64Url) throws GeneralSecurityException {
+        PublicKey publicKey = readPublicKey(publicKeyBase64Url, algorithm);
+        Signature verifier = createSignatureVerifier(publicKey, algorithm);
+        verifier.initVerify(publicKey);
+        verifier.update(decodeBase64Url(authenticatorDataBase64Url));
+        verifier.update(sha256(decodeBase64Url(clientDataJsonBase64Url)));
+        if (!verifier.verify(decodeBase64Url(signatureBase64Url))) {
+            throw new GeneralSecurityException("Invalid passkey signature");
+        }
+    }
+
+    public static void ensurePublicKeyMaterial(String publicKeyBase64Url, Integer algorithm) throws GeneralSecurityException {
+        readPublicKey(publicKeyBase64Url, algorithm);
+    }
+
+    public static byte[] decodeBase64Url(String value) {
+        if (Cools.isEmpty(value)) {
+            throw new IllegalArgumentException("Missing base64Url value");
+        }
+        return URL_DECODER.decode(String.valueOf(value).trim());
+    }
+
+    public static String buildOrigin(HttpServletRequest request) {
+        String scheme = normalizeForwardedValue(request.getHeader("X-Forwarded-Proto"));
+        if (Cools.isEmpty(scheme)) {
+            scheme = request.getScheme();
+        }
+        String host = resolveHost(request);
+        return scheme.toLowerCase(Locale.ROOT) + "://" + host;
+    }
+
+    public static String buildRpId(HttpServletRequest request) {
+        String host = resolveHost(request);
+        if (host.startsWith("[")) {
+            int bracket = host.indexOf(']');
+            return bracket > 0 ? host.substring(1, bracket).toLowerCase(Locale.ROOT) : host.toLowerCase(Locale.ROOT);
+        }
+        int colonIndex = host.indexOf(':');
+        if (colonIndex >= 0) {
+            host = host.substring(0, colonIndex);
+        }
+        return host.toLowerCase(Locale.ROOT);
+    }
+
+    public static boolean isSecureOriginAllowed(String origin, String rpId) {
+        if (Cools.isEmpty(origin) || Cools.isEmpty(rpId)) {
+            return false;
+        }
+        String lowerOrigin = origin.toLowerCase(Locale.ROOT);
+        String lowerRpId = rpId.toLowerCase(Locale.ROOT);
+        if ("localhost".equals(lowerRpId)
+                || "127.0.0.1".equals(lowerRpId)
+                || "::1".equals(lowerRpId)
+                || lowerRpId.endsWith(".localhost")) {
+            return true;
+        }
+        return lowerOrigin.startsWith("https://");
+    }
+
+    public static byte[] buildUserHandle(Long userId) {
+        return String.valueOf(userId).getBytes(StandardCharsets.UTF_8);
+    }
+
+    private static PublicKey readPublicKey(String publicKeyBase64Url, Integer algorithm) throws GeneralSecurityException {
+        byte[] encoded = decodeBase64Url(publicKeyBase64Url);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
+        List<String> keyFactories = keyFactoriesForAlgorithm(algorithm);
+        GeneralSecurityException failure = null;
+        for (String keyFactoryName : keyFactories) {
+            try {
+                return KeyFactory.getInstance(keyFactoryName).generatePublic(keySpec);
+            } catch (GeneralSecurityException ex) {
+                failure = ex;
+            }
+        }
+        throw failure == null ? new GeneralSecurityException("Unsupported passkey algorithm") : failure;
+    }
+
+    private static Signature createSignatureVerifier(PublicKey publicKey, Integer algorithm) throws GeneralSecurityException {
+        int value = algorithm == null ? Integer.MIN_VALUE : algorithm;
+        switch (value) {
+            case -7:
+                return Signature.getInstance("SHA256withECDSA");
+            case -257:
+                return Signature.getInstance("SHA256withRSA");
+            case -37:
+                Signature pss = Signature.getInstance("RSASSA-PSS");
+                pss.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1));
+                return pss;
+            case -8:
+                return Signature.getInstance("Ed25519");
+            default:
+                if ("EC".equalsIgnoreCase(publicKey.getAlgorithm())) {
+                    return Signature.getInstance("SHA256withECDSA");
+                }
+                if ("RSA".equalsIgnoreCase(publicKey.getAlgorithm())) {
+                    return Signature.getInstance("SHA256withRSA");
+                }
+                if ("Ed25519".equalsIgnoreCase(publicKey.getAlgorithm()) || "EdDSA".equalsIgnoreCase(publicKey.getAlgorithm())) {
+                    return Signature.getInstance("Ed25519");
+                }
+                throw new GeneralSecurityException("Unsupported passkey signature algorithm");
+        }
+    }
+
+    private static List<String> keyFactoriesForAlgorithm(Integer algorithm) {
+        List<String> result = new ArrayList<>();
+        int value = algorithm == null ? Integer.MIN_VALUE : algorithm;
+        switch (value) {
+            case -7:
+                result.add("EC");
+                break;
+            case -257:
+            case -37:
+                result.add("RSA");
+                break;
+            case -8:
+                result.add("Ed25519");
+                result.add("EdDSA");
+                break;
+            default:
+                result.add("EC");
+                result.add("RSA");
+                result.add("Ed25519");
+                result.add("EdDSA");
+                break;
+        }
+        return result;
+    }
+
+    private static String resolveHost(HttpServletRequest request) {
+        String host = normalizeForwardedValue(request.getHeader("X-Forwarded-Host"));
+        if (Cools.isEmpty(host)) {
+            host = request.getServerName();
+            int port = request.getServerPort();
+            if (port > 0 && port != 80 && port != 443) {
+                host = host + ":" + port;
+            }
+        }
+        String port = normalizeForwardedValue(request.getHeader("X-Forwarded-Port"));
+        if (!Cools.isEmpty(port) && host.indexOf(':') < 0 && !host.startsWith("[")) {
+            host = host + ":" + port;
+        }
+        return host;
+    }
+
+    private static String normalizeForwardedValue(String value) {
+        if (Cools.isEmpty(value)) {
+            return null;
+        }
+        String normalized = String.valueOf(value).trim();
+        int commaIndex = normalized.indexOf(',');
+        if (commaIndex >= 0) {
+            normalized = normalized.substring(0, commaIndex).trim();
+        }
+        return normalized;
+    }
+
+    private static byte[] sha256(byte[] data) throws GeneralSecurityException {
+        return MessageDigest.getInstance("SHA-256").digest(data);
+    }
+
+    public static final class AuthenticatorData {
+        private final byte[] raw;
+        private final int flags;
+        private final long signCount;
+
+        private AuthenticatorData(byte[] raw, int flags, long signCount) {
+            this.raw = raw;
+            this.flags = flags;
+            this.signCount = signCount;
+        }
+
+        public byte[] getRaw() {
+            return raw;
+        }
+
+        public int getFlags() {
+            return flags;
+        }
+
+        public long getSignCount() {
+            return signCount;
+        }
+    }
+}

--
Gitblit v1.9.1