package com.imutil.common; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.SecureRandom; import java.util.Base64; /** * 密码哈希工具(PBKDF2WithHmacSHA256,JDK 内置,无额外依赖) *
* 存储格式:{@code iterations:saltBase64:hashBase64}。 * verify 时按存储的 iterations/salt 重新推导并常量时间比对。 * 每次哈希用随机 salt,防彩虹表。 * * @author imutil */ public final class PasswordUtil { /** 迭代次数(OWASP 2023 建议 PBKDF2-HMAC-SHA256 ≥ 120000) */ private static final int ITERATIONS = 120_000; private static final int KEY_BITS = 256; private static final int SALT_BYTES = 16; private static final String ALGO = "PBKDF2WithHmacSHA256"; private PasswordUtil() { } /** * 对明文密码哈希,返回 "iterations:salt:hash" */ public static String hash(String rawPassword) { byte[] salt = new byte[SALT_BYTES]; new SecureRandom().nextBytes(salt); byte[] dk = derive(rawPassword, salt, ITERATIONS); return ITERATIONS + ":" + b64(salt) + ":" + b64(dk); } /** * 校验明文密码与存储hash是否匹配 */ public static boolean verify(String rawPassword, String stored) { if (rawPassword == null || stored == null) { return false; } String[] parts = stored.split(":"); if (parts.length != 3) { return false; } try { int iter = Integer.parseInt(parts[0]); byte[] salt = Base64.getDecoder().decode(parts[1]); byte[] expected = Base64.getDecoder().decode(parts[2]); byte[] actual = derive(rawPassword, salt, iter); return constantTimeEquals(expected, actual); } catch (Exception e) { return false; } } private static byte[] derive(String rawPassword, byte[] salt, int iter) { try { PBEKeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iter, KEY_BITS); return SecretKeyFactory.getInstance(ALGO).generateSecret(spec).getEncoded(); } catch (Exception e) { throw new IllegalStateException("PBKDF2 推导失败", e); } } private static String b64(byte[] b) { return Base64.getEncoder().encodeToString(b); } /** 常量时间比较,防时序攻击 */ private static boolean constantTimeEquals(byte[] a, byte[] b) { if (a.length != b.length) { return false; } int r = 0; for (int i = 0; i < a.length; i++) { r |= a[i] ^ b[i]; } return r == 0; } }