MD5、AES、Jasypt加密方式的简要介绍与对比
1 前言
目前做的项目中用的加密工具有:MD5、AES加密工具(旧的)及Jasypt加密工具(新增),对这三种加密工具的简要原理和应用做了整理。内容参考的文章见第6部分。如下是各工具在系统中的应用:
- MD5: 主要用在系统用户名密码的加密上
- AES:除数据库密码解密外、系统用于密码等所有的均使用该加密工具:redis、邮件、账户等
- Jasypt:目前只计划实现数据库密码的加解密,配合实现数据库自动化运维
2 MD5加密算法
2.1简介
- MD5信息摘要算法,一种被广泛使用的密码散列函数,可以产生出一个128位的散列值,用于确保信息传输完整一致。
- MD5常用于防止数据被篡改、防止直接存储明文、防止抵赖(数字签名)
2.2 原理
加密要素
- 明文信息(即不需要秘钥之类的加密要素)
唯一性
- 通过密码散列函数产生输入信息对应的128位的散列值,同一份信息原文计算后的散列值一定是一样的;
- 所以通过对比两个散列值是否一样可以判断原文信息是否被篡改;
- 但理论上来说一个MD5是可能对应无数多个原文,但是在实际现实应用中,原文长度有限,在某个范围内让两个原文对应上同一个MD5是非常困难。
不可逆性
- 密码散列函数:散列(哈希)函数的一种,即可以解释为一种单向函数,很难从函数的输出结果推出它的输入是什么;
- 算法计算过程中原文的部分信息是丢失的,所以无法将一个MD5密文进行解密(所以从解密角度看,MD5不算一个加密算法)。
2.3 应用范围
防止数据被篡改
比如在发送文件前,先传输文件使用MD5生成的MD5值,再传输文件,通过对收到的文件生成的MD5值与之前的MD5值对比来判断是否文件在传输过程中被篡改
避免直接存储明文
保存用户密码,这样即使数据库中的密码数据信息被泄露出去,也无法通过解密获得密码原文; 但是对于用户,系统根据用户输入的密码用MD5再次产生MD5值,和数据库的数据进行比较是否一致来确定用户输入的密码的合法性
数字签名
利用唯一性,例如A写了一个文件,认证机构用MD5产生摘要信息保存,只要再次对这个文件用MD5生成摘要信息,和原先保存的那份摘要信息一对比,只要一致,就证明是A写的那个文件,A无法抵赖
2.4 安全性上的问题
暴力破解风险
MD5虽然无法逆向解密,但是却可以“正向暴力破解”。 尽管暴力破解很费时间,但对于诸如密码的加密存储,往往若密码过短过简单则容易被暴力破解。很多在线工具提供通过密文匹配对应输入的原文。如后面示例,其中的一个简单密码就存在这个库中,很轻松就被找出来:
-
简单密码:用MD5生成password123456的MD5值, 用这个MD5值去查询,可以查询到原文
-
复杂密码:用MD5生成哈哈哈XSWL2333的MD5值, 用这个MD5值去查询,则目前这个在线工具还无法匹配到这个结果的原文
破解原理
实际上上述的“暴力破解”不是简单的一个一个去生成MD5值再去比较,也不是“字典法”预先构建一个庞大的明文->密文的库,而是在字典法上改进的构建彩虹表的方式。 但都本质都是通过输入MD5值得方式查找对应的原文。
安全规避方式
因此MD5应用在例如密码加密中,对于密码的要求应该不能过于简单,所以密码的设置不应过于简单; 常见的要求就是大小写字母+数字+字符,四选三来组成密码(公司手册对员工门户、个人电脑的密码要求就是如此)
2.5 项目中的应用
- 用于用户密码的加密,包括对用户密码加密后存储以及登录效验用户密码是否正确
- 封装了MD5Util工具类,提供三个加密方法直接调用:
字节数据的加密:getMD5(byte[] bytes)
字符串数据的加密:getMD5(String str)
文件的加密:getFileMD5(File file)
/**
* MD5信息-摘要算法5,是一种单向加密算法(不可逆)</br>
*/
public final class MD5Util {
//用来将字节转换成16进制表示的字符
private static char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* 私有构造函数,防止实例化使用
*/
private MD5Util() {}
/**
* 对参数进行MD5加密
* @param bytes 要加密的参数
* @return MD5值
*/
public static String getMD5(byte[] bytes) {
String result = null;
MessageDigest md = null;
try {
md = MessageDigest.getInstance("MD5");
md.update(bytes);
byte[] temp = md.digest(); //MD5的计算结果是一个128位长整数,用字节表示就是16字节
char[] chars = new char[32]; //每个字节用16进制表示的话,需要2个字符,所以共32个字符
//对MD5的每个字节转换成16进制的字符
int k = 0;
for (int i = 0; i < 16; i++) {
byte aByte = temp[i];
chars[k++] = hexDigits[aByte >>> 4 & 0xf];
chars[k++] = hexDigits[aByte & 0xf];
}
result = new String(chars);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 对参数进行MD5加密
* @param str 要加密的参数
* @return MD5值
*/
public static String getMD5(String str) {
try {
return getMD5(str.getBytes("GBK"));
} catch (UnsupportedEncodingException e) {
throw new AppException("MD5加密异常", e);
}
}
/**
* 计算文件的MD5加密值,注意如果文件较大、计算MD5时可能性能较差
* @param file 文件
* @return MD5值
*/
public static String getFileMD5(File file) {
FileInputStream in = null;
String result = null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
md.update(buffer);
}
BigInteger bigInt = new BigInteger(1, md.digest());
result = bigInt.toString(16);
} catch (Exception e) {
throw new AppException("MD5加密异常", e);
} finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e) {
throw new AppException("MD5加密时,流关闭异常", e);
}
}
return result;
}
/**
* 计算文件的MD5加密值
* @param fileName 文件名
* @return MD5值
*/
public static String getFileMD5(String fileName) {
File file = new File(fileName);
return getFileMD5(file);
}
}
3 AES加密算法
3.1 简介
- 高级加密标准(AES,Advanced Encryption Standard)
- 采用同一个秘钥进行加解密,是一种对称秘钥加密算法,加解密需要秘钥和密文/明文
3.2 原理
加密要素
- 明文/密文
- 秘钥
加解密过程
- 是一种对称加密算法:即加解密使用的是同一个秘钥,对于同一个明文,用这个秘钥加密出来的密文是一样的;反过来,相同的密文用这个秘钥解密出的结果一定是最开始的明文
什么是非对称加密算法?加解密的秘钥是不同的,例如公钥加密,只有私钥能解密。但算法复杂,加解密速度慢。
3.3 应用
- 因为是对称加密,所以加密速度快,适合大量数据的加密,且加密的数据可复原
- 所以应用场景广泛,例如用户信息,实时通信的敏感信息等
3.4 安全性上的问题
安全性
- 速度快,安全级别高:目前是最安全的加密算法之一
- 但不是绝对安全,要保证你的秘钥不被泄露,因为AES是对称加密算法,只需要知道秘钥就能解密处所有的加密算法;
安全规避方式
- 对于系统,秘钥往往写在代码里,所以如果说源代码和数据库数据泄露,那么数据就完全暴露给不法分子,若不法分子只有密文,没有秘钥,那么至少被加密的敏感数据不会被利用。 所以数据加密了并不代表就万无一失。
- 因此公司有诸如不应该把源代码传到外网等规定,目的之一就是为了防止源代码泄露导致重要或敏感数据泄露
3.5 系统中的应用
- redis、邮件、Eterm账户等密码或其他信息的加密,应用最多
- 封装工具类:AESUtil
- 私有构造函数:制定加密算法,生成Key
- 加解密只需要密文/明文,返回结果
- 加密:getEncString(String str)
- 解密:getDesString(String str)
/**
* AES加密算法,采用同一个密钥进行加解密,是一种对称密钥加密算法。
*/
public final class AESUtil {
//自己定义的秘钥,128bit即16位的随机串。也支持192,25bit,长度越长安全性越高,对应的加解密时间越长
private final static String KEY_STR = "gOZ+l59TRoBajn3G";
private final Key key;
private static AESUtil instance = new AESUtil();
private static final String ALGORITHM = "AES";
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
/**
* 私有构造函数,防止通过实例化使用
*/
private AESUtil() {
try {
KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);//Java的秘钥生产器,使用的是AES
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);//随机数的算法,NativePRNG和SHA1PRNG
random.setSeed(KEY_STR.getBytes("GBK"));//用我们设定的秘钥串作为随机数的种子,因为种子是我们固定的,产生的随机数也是固定的
generator.init(random);
key = generator.generateKey();//生成的秘钥,我们在加密解密需要用到相同秘钥
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new AppException("AES构造函数异常", e);
}
}
public static AESUtil getInstance() {
return instance;
}
/**
* 对byte[]参数进行加密
* @param bytes 要加密的参数
* @return 加密后的参数
*/
private byte[] getEncCode(byte[] bytes) {
byte[] result = null;
Cipher cipher = null;
try {
cipher = Cipher.getInstance("AES");//获取算法实例
cipher.init(Cipher.ENCRYPT_MODE, key);//初始化,入参为加密模式和秘钥
result = cipher.doFinal(bytes);//进行加密
} catch (Exception e) {
throw new AppException("AES加密异常", e);
} finally {
cipher = null;
}
return result;
}
/**
* 对byte[]参数进行解密
* @param bytes 要解密的参数
* @return 解密后的参数
*/
private byte[] getDesCode(byte[] bytes) {
byte[] result = null;
Cipher cipher = null;
try {
cipher = Cipher.getInstance("AES");//获取算法实例
cipher.init(Cipher.DECRYPT_MODE, key);//初始化,入参为解密模式和秘钥
result = cipher.doFinal(bytes);//进行解密
} catch (Exception e) {
throw new AppException("AES解密异常", e);
} finally {
cipher = null;
}
return result;
}
/**
* 对string参数进行加密
* @param str 要加密的参数
* @return 加密后的参数
*/
public String getEncString(String str) {
BASE64Encoder base64en = new BASE64Encoder();
byte[] input = null; //明文
byte[] output = null; //密文
String result = null;
try {
input = str.getBytes(CommonConstant.CHARSET_NAME);
output = getEncCode(input);
result = base64en.encode(output);
} catch (Exception e) {
throw new AppException("AES解密异常,参数:" + str, e);
} finally {
input = null;
output = null;
}
return result;
}
/**
* 对String参数进行解密
* @param str 要解密的参数
* @return 解密后的参数
*/
public String getDesString(String str) {
BASE64Decoder base64De = new BASE64Decoder();
byte[] input = null; //密文
byte[] output = null; //明文
String result = null;
try {
input = base64De.decodeBuffer(str);
output = getDesCode(input);
result = new String(output, CommonConstant.CHARSET_NAME);
} catch (Exception e) {
throw new AppException("AES解密异常,参数:" + str, e);
} finally {
input = null;
output = null;
}
return result;
}
}
4 Jasypt加密工具
4.1 简介
Jasypt是一个开源的java库,只需要引入组件就可以使用,开发中无需关注其太多细节即可实现java项目的加密功能; 也因此很难找到相关的资料,因此原理也基本是自己使用和测试结果的总结,不一定严谨。
4.2 原理
加密要素
密文/明文:password
系统钥: sysKey
秘钥:secretKey
加密机制
- 一套sysKey和secretKey生产的密文是“动态”的(即同一个明文每次加密后的结果不一样); 但这些密文用这一套sysKey和secretKey解密处的结果都一样;
- 同一套sysKey、secretKey加密后的密文是“固定”的; (准确说不是由这一套加密出来的密文解密不出东西,会报错)
- 同一系统拥同一套sysKey和secretKey(目前仅是公司DBA那边的规定)
- 加解密需要三个加密要素,直接调用,无需其他步骤
4.3 应用范围
- 密码、敏感数据、甚至文件、应用程序
- 因为无需关注其细节,容易使用,很适合在java项目中实现基本的加密要求
4.4 安全性上的问题
安全规避方式
- 和AES都是对称加密算法的一种,一样要保证secretKey和sysKey的安全
- 和AES有点不一样的是,Jasypt加解密需要sysKey和secretKey,所以在系统中使用时,要避免将这两者放在一起,例如都写在一个配置文件
4.5 系统中的应用
- 目前主要用于数据库连接,对数据库密码的加密,配合公司对数据库安全的要求。
- password和sysKey由DBA管理提供,自助下载
- secretKey 由DBA提供(配合Apollo配置中心可以实现开发人员无法接触到secretKey,在一些安全管理实践上很方便)
- 封装工具类JasyptForDbUtil
- 使用到原生工具:JasyptUtil(直接调用,无需其他步骤)
public String encryptString(String sysKey, String secretKey, String str)
public String decryptString(String sysKey, String secretKey, String encString) - 秘钥写在代码里,与sysKey和密文分开,避免都在一个地方
private final static String SECRET_KEY = “aSecretKey”; - 加密:public String getEncStr(String sysKey, String plainText)
- 解密:public String getDesStr(String sysKey, String cipherText)
/**
* Jasypt 加密算法,用于数据库密码加解密 <br>
* 加解密需要 sysKey、secretKey、密文或明文 <br>
* 加密生成的密码是动态的,但与同一组sysKey、secretKey是对应的 <br>
*/
public final class JasyptUtil {
/**
* 秘钥,不同系统的秘钥不同 <br>
* 秘钥与配置文件密文分开,避免都在一个地方 <br>
*/
private final static String SECRET_KEY = "mySrcretKey";
private static com.mytest.jasypt.util.JasyptUtil jasyptUtil = new com.mytest.jasypt.util.JasyptUtil();
private static JasyptUtil instance = new JasyptUtil();
public static JasyptUtil getInstance() {
return instance;
}
/**
* 私有构造函数,防止通过实例化使用
*/
private JasyptUtil() {
}
/**
* Jasypt 加密
* @param sysKey sysKey
* @param plainText 要加密的明文
* @return 加密后的字符串
*/
public String getEncStr(String sysKey, String plainText) {
try {
return jasyptUtil.encryptString(sysKey, SECRET_KEY, plainText);
} catch (Exception e) {
throw new AppException("Jasypt加密异常" + e);
}
}
/**
* Jasypt 解密
* @param sysKey sysKey
* @param cipherText 要解密的密文
* @return 解密密后的字符串
*/
public String getDesStr(String sysKey, String cipherText) {
try {
return jasyptUtil.decryptString(sysKey, SECRET_KEY, cipherText);
} catch (Exception e) {
throw new AppException("Jasypt解密异常" + e);
}
}
}
4.6延伸:实现对配置文件属性的解密(使用Jasypt)
系统中加密工具一个重要的应用是对一些配置文件的信息进行解密,例如数据库连接的密码使用密文保存在配置文件中,需要解密后使用
1 读取配置文件,对配置文件属性进行解密并重置操作
重写PropertyResourceConfigurer类的convertProperties方法,启动时执行该方法,读取配置文件所有属性,并解密重设需解密的属性值; 该方法由PropertyResourceConfigurer自身提供缺省实现,用于对属性值做必要的转换处理,缺省不做任何处理;
是否解密通过是否存在同名属性名+.encode,属性值为对应加密算法,根据配置的加密算法值使用对应加密工具进行解密
伪代码
@Override
protected void convertProperties(Properties props) {
//获取所有配置文件属性信息
Enumeration propertyNames = props.propertyNames();
while (propertyNames.hasMoreElements()) {
//循环这些属性
if (name.endsWith(".encode")) {
value = props.getProperty(name); //获取加密算法,看采用哪种加密工具进行解密
name = name.substring(0, name.indexOf(".encode")); //找待解密参数名
value = decode(name, value, props); //进行解密,获取解密后的参数值
props.setProperty(name, value);//用解密结果重设属性值
}
}
}
2 修改相关配置文件
<!-- Spring修改相关配置文件-->
<bean id="propertyConfigurer" class="com.mytest.common.config.DecryptPropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:public.properties</value>
<value>classpath:jdbc.properties</value>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
# public.properties配置文件
# jasypt加密使用到的sysKey,同一系统sysKey相同
jasypt.sysKey=sysKey串
# jdbc.properties配置文件
oracle.name=dbname
oracle.username=username
oracle.password=密文
oracle.password.encode=JASYPT