|
@@ -0,0 +1,214 @@
|
|
|
|
|
+package com.fs.common;
|
|
|
|
|
+
|
|
|
|
|
+import javax.crypto.Cipher;
|
|
|
|
|
+import javax.crypto.KeyGenerator;
|
|
|
|
|
+import javax.crypto.SecretKey;
|
|
|
|
|
+import javax.crypto.spec.GCMParameterSpec;
|
|
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.SecureRandom;
|
|
|
|
|
+import java.util.Base64;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * AES-256-GCM 加解密工具类
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * GCM(Galois/Counter Mode)同时提供机密性和完整性保护,自带认证标签(Auth Tag),
|
|
|
|
|
+ * 相比 CBC/ECB 模式可同时防篡改和防重放,是当前推荐的对称加密模式。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 密文格式(Base64):IV(12字节) + 密文 + AuthTag(16字节)
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <h3>使用示例</h3>
|
|
|
|
|
+ * <pre>{@code
|
|
|
|
|
+ * // 1. 生成密钥(一次性,妥善保存)
|
|
|
|
|
+ * String key = AesGcm256Utils.generateKey();
|
|
|
|
|
+ *
|
|
|
|
|
+ * // 2. 加密
|
|
|
|
|
+ * String cipher = AesGcm256Utils.encrypt("敏感数据", key);
|
|
|
|
|
+ *
|
|
|
|
|
+ * // 3. 解密
|
|
|
|
|
+ * String plain = AesGcm256Utils.decrypt(cipher, key);
|
|
|
|
|
+ *
|
|
|
|
|
+ * // 4. 带 AAD(附加认证数据)加解密——AAD 不加密,但参与认证,防止密文被挪用
|
|
|
|
|
+ * String cipher2 = AesGcm256Utils.encrypt("敏感数据", key, "orderId=123");
|
|
|
|
|
+ * String plain2 = AesGcm256Utils.decrypt(cipher2, key, "orderId=123");
|
|
|
|
|
+ * }</pre>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author fs
|
|
|
|
|
+ */
|
|
|
|
|
+public class AesGcm256Utils {
|
|
|
|
|
+
|
|
|
|
|
+ /** AES 密钥位数:256 位(32 字节) */
|
|
|
|
|
+ private static final int KEY_BIT_LENGTH = 256;
|
|
|
|
|
+
|
|
|
|
|
+ /** GCM 模式 IV(Nonce)长度:12 字节(96 位),GCM 规范推荐值 */
|
|
|
|
|
+ private static final int IV_BYTE_LENGTH = 12;
|
|
|
|
|
+
|
|
|
|
|
+ /** GCM 认证标签长度:128 位(16 字节),GCM 规范推荐的最大值 */
|
|
|
|
|
+ private static final int AUTH_TAG_BIT_LENGTH = 128;
|
|
|
|
|
+
|
|
|
|
|
+ /** 加密算法完整标识:AES/GCM/NoPadding */
|
|
|
|
|
+ private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
|
|
|
|
+
|
|
|
|
|
+ /** 密钥算法:AES */
|
|
|
|
|
+ private static final String KEY_ALGORITHM = "AES";
|
|
|
|
|
+
|
|
|
|
|
+ /** 安全随机数生成器(线程安全) */
|
|
|
|
|
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
|
|
|
|
+
|
|
|
|
|
+ private AesGcm256Utils() {
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成 256 位 AES 密钥,返回 Base64 编码字符串
|
|
|
|
|
+ * <p>生成后请妥善保存,加解密需使用同一密钥。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return Base64 编码的密钥
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String generateKey() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
|
|
|
|
|
+ keyGenerator.init(KEY_BIT_LENGTH, SECURE_RANDOM);
|
|
|
|
|
+ SecretKey secretKey = keyGenerator.generateKey();
|
|
|
|
|
+ return Base64.getEncoder().encodeToString(secretKey.getEncoded());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException("生成 AES-256 密钥失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从 Base64 编码字符串加载密钥
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param keyBase64 Base64 编码的密钥
|
|
|
|
|
+ * @return SecretKey 对象
|
|
|
|
|
+ */
|
|
|
|
|
+ public static SecretKey loadKey(String keyBase64) {
|
|
|
|
|
+ byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
|
|
|
|
|
+ if (keyBytes.length != 32) {
|
|
|
|
|
+ throw new IllegalArgumentException("AES-256 密钥必须为 32 字节,当前长度:" + keyBytes.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new SecretKeySpec(keyBytes, KEY_ALGORITHM);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加密(无 AAD)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param plainText 明文
|
|
|
|
|
+ * @param keyBase64 Base64 编码的密钥
|
|
|
|
|
+ * @return Base64 编码的密文(格式:IV + 密文 + AuthTag)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String encrypt(String plainText, String keyBase64) {
|
|
|
|
|
+ return encrypt(plainText, keyBase64, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加密(带 AAD 附加认证数据)
|
|
|
|
|
+ * <p>AAD 不被加密,但会参与认证标签计算,解密时需传入相同 AAD 才能通过认证。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param plainText 明文
|
|
|
|
|
+ * @param keyBase64 Base64 编码的密钥
|
|
|
|
|
+ * @param aad 附加认证数据(可为 null)
|
|
|
|
|
+ * @return Base64 编码的密文(格式:IV + 密文 + AuthTag)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String encrypt(String plainText, String keyBase64, String aad) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
+ byte[] ivBytes = new byte[IV_BYTE_LENGTH];
|
|
|
|
|
+ SECURE_RANDOM.nextBytes(ivBytes);
|
|
|
|
|
+
|
|
|
|
|
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
|
|
|
|
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(AUTH_TAG_BIT_LENGTH, ivBytes);
|
|
|
|
|
+ cipher.init(Cipher.ENCRYPT_MODE, loadKey(keyBase64), parameterSpec);
|
|
|
|
|
+
|
|
|
|
|
+ if (aad != null && !aad.isEmpty()) {
|
|
|
|
|
+ cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ byte[] cipherTextWithTag = cipher.doFinal(plainBytes);
|
|
|
|
|
+
|
|
|
|
|
+ // 拼接:IV(12) + 密文+AuthTag
|
|
|
|
|
+ byte[] output = new byte[IV_BYTE_LENGTH + cipherTextWithTag.length];
|
|
|
|
|
+ System.arraycopy(ivBytes, 0, output, 0, IV_BYTE_LENGTH);
|
|
|
|
|
+ System.arraycopy(cipherTextWithTag, 0, output, IV_BYTE_LENGTH, cipherTextWithTag.length);
|
|
|
|
|
+
|
|
|
|
|
+ return Base64.getEncoder().encodeToString(output);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException("AES-256-GCM 加密失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解密(无 AAD)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param cipherTextBase64 Base64 编码的密文(格式:IV + 密文 + AuthTag)
|
|
|
|
|
+ * @param keyBase64 Base64 编码的密钥
|
|
|
|
|
+ * @return 明文
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String decrypt(String cipherTextBase64, String keyBase64) {
|
|
|
|
|
+ return decrypt(cipherTextBase64, keyBase64, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解密(带 AAD 附加认证数据)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param cipherTextBase64 Base64 编码的密文(格式:IV + 密文 + AuthTag)
|
|
|
|
|
+ * @param keyBase64 Base64 编码的密钥
|
|
|
|
|
+ * @param aad 附加认证数据(需与加密时一致,可为 null)
|
|
|
|
|
+ * @return 明文
|
|
|
|
|
+ * @throws RuntimeException 认证失败(密钥错误、密文被篡改、AAD 不一致)时抛出
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String decrypt(String cipherTextBase64, String keyBase64, String aad) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ byte[] input = Base64.getDecoder().decode(cipherTextBase64);
|
|
|
|
|
+ if (input.length < IV_BYTE_LENGTH) {
|
|
|
|
|
+ throw new IllegalArgumentException("密文长度非法,少于 IV 长度");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ byte[] ivBytes = new byte[IV_BYTE_LENGTH];
|
|
|
|
|
+ byte[] cipherTextWithTag = new byte[input.length - IV_BYTE_LENGTH];
|
|
|
|
|
+ System.arraycopy(input, 0, ivBytes, 0, IV_BYTE_LENGTH);
|
|
|
|
|
+ System.arraycopy(input, IV_BYTE_LENGTH, cipherTextWithTag, 0, cipherTextWithTag.length);
|
|
|
|
|
+
|
|
|
|
|
+ Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
|
|
|
|
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(AUTH_TAG_BIT_LENGTH, ivBytes);
|
|
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, loadKey(keyBase64), parameterSpec);
|
|
|
|
|
+
|
|
|
|
|
+ if (aad != null && !aad.isEmpty()) {
|
|
|
|
|
+ cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ byte[] plainBytes = cipher.doFinal(cipherTextWithTag);
|
|
|
|
|
+ return new String(plainBytes, StandardCharsets.UTF_8);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException("AES-256-GCM 解密失败(密钥错误、密文被篡改或 AAD 不一致)", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自测主方法
|
|
|
|
|
+ */
|
|
|
|
|
+ public static void main(String[] args) {
|
|
|
|
|
+ String key = generateKey();
|
|
|
|
|
+ System.out.println("生成的密钥:" + key);
|
|
|
|
|
+
|
|
|
|
|
+ String plain = "医健宝敏感数据-test-123456";
|
|
|
|
|
+ String cipher = encrypt(plain, key);
|
|
|
|
|
+ System.out.println("加密结果:" + cipher);
|
|
|
|
|
+ System.out.println("解密结果:" + decrypt(cipher, key));
|
|
|
|
|
+
|
|
|
|
|
+ // 带 AAD 测试
|
|
|
|
|
+ String aad = "orderId=888";
|
|
|
|
|
+ String cipher2 = encrypt(plain, key, aad);
|
|
|
|
|
+ System.out.println("带 AAD 加密结果:" + cipher2);
|
|
|
|
|
+ System.out.println("带 AAD 解密结果:" + decrypt(cipher2, key, aad));
|
|
|
|
|
+
|
|
|
|
|
+ // AAD 不一致应抛异常
|
|
|
|
|
+ try {
|
|
|
|
|
+ decrypt(cipher2, key, "orderId=999");
|
|
|
|
|
+ System.out.println("【错误】AAD 不一致却解密成功!");
|
|
|
|
|
+ } catch (RuntimeException e) {
|
|
|
|
|
+ System.out.println("【正确】AAD 不一致解密失败:" + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|