目录
一、序言
二、关于前端CryptoJS
1、CryptoJS简单介绍
2、加密和填充模式选择
3、前端AES加解密示例
(1) cryptoutils工具类
(2) 测试用例
(3) 加解密后输出内容说明
三、Java后端AES加解密
1、Java中支持的加密模式和填充说明
2、工具类CryptoUtils
3、测试用例
一、序言
最近刚好在做一个简单的保险代理人运营平台,主要是为了方便个人展业,由于有些客户数据比较敏感,所以在用户登录时准备对登录密码进行一波加密后再传输。
这里我准备用AES对称加密算法对数据进行加密传输,经过调研后前端加解密相关准备用CryptoJS,后端加解密工具类根据JDK1.8官方文档自己来实现。
二、关于前端CryptoJS
1、CryptoJS简单介绍
CryptoJS是标准安全加密算法的JavaScript实现,运行速度快,接口简单,见名知意。文档说明可参考:CryptoJS文档说明。
CryptoJS的实现主要有哈希算法,HMAC、加密算法、以及常用编解码算法。
哈希算法,如MD5、SHA-1、SHA-2等。
加密算法,如AES、DES等。
编解码算法,如Base64、Hex16等。
2、加密和填充模式选择
下图是CryptoJS支持的加密模式和填充模式,CryptoJS默认的加密模式为CBC,填充模式为Pkcs7。
常用的加密模式有CBC和ECB,由于CBC安全性更高,所以我们还是选用CBC和Pkcs7的组合。
3、前端AES加解密示例
(1) cryptoutils工具类
这里我封装了一个CryptoJS工具类
import CryptoJS from 'crypto-js' /** * AES加密 * @param plainText 明文 * @param keyInBase64Str base64编码后的key * @param ivInBase64Str base64编码后的初始化向量(只有CBC模式下才支持) * @return base64编码后的密文 */ export function encryptByAES(plainText, keyInBase64Str, ivInBase64Str) { let key = CryptoJS.enc.Base64.parse(keyInBase64Str) let iv = CryptoJS.enc.Base64.parse(ivInBase64Str) let encrypted = CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) // 这里的encrypted不是字符串,而是一个CipherParams对象 return encrypted.ciphertext.toString(CryptoJS.enc.Base64) } /** * AES解密 * @param cipherText 密文 * @param keyInBase64Str base64编码后的key * @param ivInBase64Str base64编码后的初始化向量(只有CBC模式下才支持) * @return 明文 */ export function decryptByAES(cipherText, keyInBase64Str, ivInBase64Str) { let key = CryptoJS.enc.Base64.parse(keyInBase64Str) let iv = CryptoJS.enc.Base64.parse(ivInBase64Str) // 返回的是一个Word Array Object,其实就是Java里的字节数组 let decrypted = CryptoJS.AES.decrypt(cipherText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) return decrypted.toString(CryptoJS.enc.Utf8) }
(2) 测试用例
let secret = 'sQPoC/1do9BZMkg8I5c09A==' let cipherText = encryptByAES('Hello', secret, secret) let plainText = decryptByAES(cipherText, secret, secret) console.log(`Hello加密后的密文为:${cipherText}`) console.log(`解密后的内容为:${plainText}`)
控制台输出内容如下:
Hello加密后的密文为:3IDpt0VzxmAv10qvQRubFQ==
解密后的内容为:Hello
(3) 加解密后输出内容说明
根据官方文档,解密后的明文是一个WordArray对象,也就是我们所说的Java中的字节数组,该对象有一个toString方法可以转换成16进制、Base64、UTF8等字符串。
加密后的密文也不是字符串,而是一个CipherParams对象,其中的ciphertext属性也是一个 WordArray对象。
三、Java后端AES加解密
1、Java中支持的加密模式和填充说明
前面我们前端AES加密模式用的是CBC,填充模式时是PKCS7。
现在我们先来看看Java中支持的加密模式和填充,在Java中这些被称为transformation,具体支持哪些transformation,请参考:JDK8中加密算法实现要求。
根据上图,我们只要算法、加密模式和填充模式和前端保持一致就行了,这里我们选用的transformation为AES/CBC/PKCS5Padding (128)
。
备注:前端CryptoJS的填充模式为PKCS7,而Java中的填充模式为PKCS5,但这里并不会有啥影响。
2、工具类CryptoUtils
这里我自己封装了一个工具类,支持AES、DES、RSA、数字签名与校验等。
/** * 支持AES、DES、RSA加密、数字签名以及生成对称密钥和非对称密钥对 */ public class CryptoUtils { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Decoder BASE64_DECODER = Base64.getDecoder(); private static final Map<Algorithm, KeyFactory> KEY_FACTORY_CACHE = new ConcurrentHashMap<>(); private static final Map<Algorithm, Cipher> CIPHER_CACHE = new HashMap<>(); /** * 生成对称密钥,目前支持的算法有AES、DES * @param algorithm * @return * @throws NoSuchAlgorithmException */ public static String generateSymmetricKey(Algorithm algorithm) throws NoSuchAlgorithmException { KeyGenerator generator = KeyGenerator.getInstance(algorithm.getName()); generator.init(algorithm.getKeySize()); SecretKey secretKey = generator.generateKey(); return BASE64_ENCODER.encodeToString(secretKey.getEncoded()); } /** * 生成非对称密钥对,目前支持的算法有RSA、DSA * @param algorithm * @return * @throws NoSuchAlgorithmException */ public static AsymmetricKeyPair generateAsymmetricKeyPair(Algorithm algorithm) throws NoSuchAlgorithmException { KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.getName()); generator.initialize(algorithm.getKeySize()); KeyPair keyPair = generator.generateKeyPair(); String publicKey = BASE64_ENCODER.encodeToString(keyPair.getPublic().getEncoded()); String privateKey = BASE64_ENCODER.encodeToString(keyPair.getPrivate().getEncoded()); return new AsymmetricKeyPair(publicKey, privateKey); } public static String encryptByRSA(String publicKeyText, String plainText) throws Exception { return encryptAsymmetrically(publicKeyText, plainText, Algorithm.RSA_ECB_PKCS1); } public static String decryptByRSA(String privateKeyText, String ciphertext) throws Exception { return decryptAsymmetrically(privateKeyText, ciphertext, Algorithm.RSA_ECB_PKCS1); } /** * SHA1签名算法和DSA加密算法结合使用生成数字签名 * @param privateKeyText * @param msg * @return 数字签名 * @throws Exception */ public static String signBySHA1WithDSA(String privateKeyText, String msg) throws Exception { return doSign(privateKeyText, msg, Algorithm.DSA, Algorithm.SHA1WithDSA); } /** * SHA1签名算法和RSA加密算法结合使用生成数字签名 * @param privateKeyText 私钥 * @param msg 待加签内容 * @return 数字签名 * @throws Exception */ public static String signBySHA1WithRSA(String privateKeyText, String msg) throws Exception { return doSign(privateKeyText, msg, Algorithm.RSA_ECB_PKCS1, Algorithm.SHA1WithRSA); } /** * SHA256签名算法和RSA加密算法结合使用生成数字签名 * @param privateKeyText 私钥 * @param msg 待加签内容 * @return 数字签名 * @throws Exception */ public static String signBySHA256WithRSA(String privateKeyText, String msg) throws Exception { return doSign(privateKeyText, msg, Algorithm.RSA_ECB_PKCS1, Algorithm.SHA256WithRSA); } /** * SHA1签名算法和DSA加密算法检验数字签名 * @param publicKeyText 公钥 * @param msg 待验签内容 * @param signatureText 数字 * @return 检验是否成功 * @throws Exception */ public static boolean verifyBySHA1WithDSA(String publicKeyText, String msg, String signatureText) throws Exception { return doVerify(publicKeyText, msg, signatureText, Algorithm.DSA, Algorithm.SHA1WithDSA); } /** * SHA1签名算法和RSA加密算法检验数字签名 * @param publicKeyText 公钥 * @param msg 待验签内容 * @param signatureText 签名 * @return 校验是否成功 * @throws Exception */ public static boolean verifyBySHA1WithRSA(String publicKeyText, String msg, String signatureText) throws Exception { return doVerify(publicKeyText, msg, signatureText, Algorithm.RSA_ECB_PKCS1, Algorithm.SHA1WithRSA); } /** * SHA256签名算法和RSA加密算法检验数字签名 * @param publicKeyText 公钥 * @param msg 待验签内容 * @param signatureText 签名 * @return 校验是否成功 * @throws Exception */ public static boolean verifyBySHA256WithRSA(String publicKeyText, String msg, String signatureText) throws Exception { return doVerify(publicKeyText, msg, signatureText, Algorithm.RSA_ECB_PKCS1, Algorithm.SHA256WithRSA); } /** * 对称加密 * @param secretKey 密钥 * @param iv 加密向量,只有CBC模式才支持 * @param plainText 明文 * @param algorithm 对称加密算法,如AES、DES * @return * @throws Exception */ public static String encryptSymmetrically(String secretKey, String iv, String plainText, Algorithm algorithm) throws Exception { SecretKey key = decodeSymmetricKey(secretKey, algorithm); IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv); byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET); byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, key, ivParameterSpec, plainTextInBytes); return BASE64_ENCODER.encodeToString(ciphertextInBytes); } /** * 对称解密 * @param secretKey 密钥 * @param iv 加密向量,只有CBC模式才支持 * @param ciphertext 密文 * @param algorithm 对称加密算法,如AES、DES * @return * @throws Exception */ public static String decryptSymmetrically(String secretKey, String iv, String ciphertext, Algorithm algorithm) throws Exception { SecretKey key = decodeSymmetricKey(secretKey, algorithm); IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv); byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext); byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, key, ivParameterSpec, ciphertextInBytes); return new String(plainTextInBytes, DEFAULT_CHARSET); } /** * 非对称加密 * @param publicKeyText 公钥 * @param plainText 明文 * @param algorithm 非对称加密算法 * @return * @throws Exception */ public static String encryptAsymmetrically(String publicKeyText, String plainText, Algorithm algorithm) throws Exception { PublicKey publicKey = regeneratePublicKey(publicKeyText, algorithm); byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET); byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, publicKey, plainTextInBytes); return BASE64_ENCODER.encodeToString(ciphertextInBytes); } /** * 非对称解密 * @param privateKeyText 私钥 * @param ciphertext 密文 * @param algorithm 非对称加密算法 * @return * @throws Exception */ public static String decryptAsymmetrically(String privateKeyText, String ciphertext, Algorithm algorithm) throws Exception { PrivateKey privateKey = regeneratePrivateKey(privateKeyText, algorithm); byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext); byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, privateKey, ciphertextInBytes); return new String(plainTextInBytes, DEFAULT_CHARSET); } /** * 生成数字签名 * @param privateKeyText 私钥 * @param msg 传输的数据 * @param encryptionAlgorithm 加密算法,见Algorithm中的加密算法 * @param signatureAlgorithm 签名算法,见Algorithm中的签名算法 * @return 数字签名 * @throws Exception */ public static String doSign(String privateKeyText, String msg, Algorithm encryptionAlgorithm, Algorithm signatureAlgorithm) throws Exception { PrivateKey privateKey = regeneratePrivateKey(privateKeyText, encryptionAlgorithm); // Signature只支持签名算法 Signature signature = Signature.getInstance(signatureAlgorithm.getName()); signature.initSign(privateKey); signature.update(msg.getBytes(DEFAULT_CHARSET)); byte[] signatureInBytes = signature.sign(); return BASE64_ENCODER.encodeToString(signatureInBytes); } /** * 数字签名验证 * @param publicKeyText 公钥 * @param msg 传输的数据 * @param signatureText 数字签名 * @param encryptionAlgorithm 加密算法,见Algorithm中的加密算法 * @param signatureAlgorithm 签名算法,见Algorithm中的签名算法 * @return 校验是否成功 * @throws Exception */ public static boolean doVerify(String publicKeyText, String msg, String signatureText, Algorithm encryptionAlgorithm, Algorithm signatureAlgorithm) throws Exception { PublicKey publicKey = regeneratePublicKey(publicKeyText, encryptionAlgorithm); Signature signature = Signature.getInstance(signatureAlgorithm.getName()); signature.initVerify(publicKey); signature.update(msg.getBytes(DEFAULT_CHARSET)); return signature.verify(BASE64_DECODER.decode(signatureText)); } /** * 将密钥进行Base64位解码,重新生成SecretKey实例 * @param secretKey 密钥 * @param algorithm 算法 * @return */ private static SecretKey decodeSymmetricKey(String secretKey, Algorithm algorithm) { byte[] key = BASE64_DECODER.decode(secretKey); return new SecretKeySpec(key, algorithm.getName()); } private static IvParameterSpec decodeIv(String iv) { byte[] ivInBytes = BASE64_DECODER.decode(iv); return new IvParameterSpec(ivInBytes); } private static PublicKey regeneratePublicKey(String publicKeyText, Algorithm algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException { byte[] keyInBytes = BASE64_DECODER.decode(publicKeyText); KeyFactory keyFactory = getKeyFactory(algorithm); // 公钥必须使用RSAPublicKeySpec或者X509EncodedKeySpec KeySpec publicKeySpec = new X509EncodedKeySpec(keyInBytes); PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); return publicKey; } private static PrivateKey regeneratePrivateKey(String key, Algorithm algorithm) throws Exception { byte[] keyInBytes = BASE64_DECODER.decode(key); KeyFactory keyFactory = getKeyFactory(algorithm); // 私钥必须使用RSAPrivateCrtKeySpec或者PKCS8EncodedKeySpec KeySpec privateKeySpec = new PKCS8EncodedKeySpec(keyInBytes); PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); return privateKey; } private static KeyFactory getKeyFactory(Algorithm algorithm) throws NoSuchAlgorithmException { KeyFactory keyFactory = KEY_FACTORY_CACHE.get(algorithm); if (keyFactory == null) { keyFactory = KeyFactory.getInstance(algorithm.getName()); KEY_FACTORY_CACHE.put(algorithm, keyFactory); } return keyFactory; } private static byte[] transform(Algorithm algorithm, int mode, Key key, byte[] msg) throws Exception { return transform(algorithm, mode, key, null, msg); } private static byte[] transform(Algorithm algorithm, int mode, Key key, IvParameterSpec iv, byte[] msg) throws Exception { Cipher cipher = CIPHER_CACHE.get(algorithm); // double check,减少上下文切换 if (cipher == null) { synchronized (CryptoUtils.class) { if ((cipher = CIPHER_CACHE.get(algorithm)) == null) { cipher = determineWhichCipherToUse(algorithm); CIPHER_CACHE.put(algorithm, cipher); } cipher.init(mode, key, iv); return cipher.doFinal(msg); } } synchronized (CryptoUtils.class) { cipher.init(mode, key, iv); return cipher.doFinal(msg); } } private static Cipher determineWhichCipherToUse(Algorithm algorithm) throws NoSuchAlgorithmException, NoSuchPaddingException { Cipher cipher; String transformation = algorithm.getTransformation(); // 官方推荐的transformation使用algorithm/mode/padding组合,SunJCE使用ECB作为默认模式,使用PKCS5Padding作为默认填充 if (StringUtils.isNotEmpty(transformation)) { cipher = Cipher.getInstance(transformation); } else { cipher = Cipher.getInstance(algorithm.getName()); } return cipher; } /** * 算法分为加密算法和签名算法,更多算法实现见:<br/> * <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#impl">jdk8中的标准算法</a> */ @Data @NoArgsConstructor @AllArgsConstructor public static class Algorithm { /** * 以下为加密算法,加密算法transformation采用algorithm/mode/padding的形式 */ public static final Algorithm AES_ECB_PKCS5 = new Algorithm("AES", "AES/ECB/PKCS5Padding", 128); public static final Algorithm AES_CBC_PKCS5 = new Algorithm("AES", "AES/CBC/PKCS5Padding", 128); public static final Algorithm DES_ECB_PKCS5 = new Algorithm("DES", "DES/ECB/PKCS5Padding", 56); public static final Algorithm DES_CBC_PKCS5 = new Algorithm("DES", "DES/CBC/PKCS5Padding", 56); public static final Algorithm RSA_ECB_PKCS1 = new Algorithm("RSA", "RSA/ECB/PKCS1Padding", 1024); /** * 以下为签名算法 */ public static final Algorithm DSA = new Algorithm("DSA", 1024); public static final Algorithm SHA1WithDSA = new Algorithm("SHA256WithRSA", 1024); public static final Algorithm SHA1WithRSA = new Algorithm("SHA1WithRSA", 2048); public static final Algorithm SHA256WithRSA = new Algorithm("SHA256WithRSA", 2048); private String name; private String transformation; private int keySize; public Algorithm(String name, int keySize) { this(name, null, keySize); } } @Data @NoArgsConstructor @AllArgsConstructor public static class AsymmetricKeyPair { private String publicKey; private String privateKey; } }
3、测试用例
import com.universe.crypto.CryptoUtils.Algorithm; /** * @author 刘亚楼 * @date 2022/6/12 */ public class AESDemo { public static void main(String[] args) throws Exception { String key = CryptoUtils.generateSymmetricKey(Algorithm.AES_CBC_PKCS5); System.out.println("生成的key为:" + key); String cipherText = CryptoUtils.encryptSymmetrically(key, key, "Hello", Algorithm.AES_CBC_PKCS5); System.out.println("加密后的密文为:" + cipherText); String plainText = CryptoUtils.decryptSymmetrically(key, key, cipherText, Algorithm.AES_CBC_PKCS5); System.out.println("解密后的明文为:" + plainText); } }
控制台输出如下:
生成的key为:sQPoC/1do9BZMkg8I5c09A==
加密后的密文为:3IDpt0VzxmAv10qvQRubFQ==
解密后的明文为:Hello