MD5
加密与摘要是不一样的,加密的数据是完整的,通过解密可以获取完整的数据。
摘要得到的消息是不完整的,通过摘要的数据不能获取到原数据。
MD5
长度默认是128bit,这样表达不好,所以将二级制转换成16进制,4bit代表一个16进制,所有128/4=32 ,所以为32位16进制。
MD5 16位与32位区别是将32位后面的16位去掉,得到的16位
MD5作用
- 一致性检验
- 数字签名
- 安全访问,就是对数据加密存到数据库或服务器中,只有对应的密钥才能访问
MD5是不可逆的,没有对应的算法,无法从生产MD5值逆向获取到原数据,除非采用暴力破解,彩虹表法。
MD5不可逆原因
由于它是一种散列数,也叫哈希数,它是一种单向密码体制,即明文到密文不可逆映射,即只有加密过程,没有解密过程。哈希函数可以将任意长度的输入变化成固定长度的输入,针对不同的输入得到不同的输出,如果两个不同的消息得到相同的哈希值,就称为碰撞,它具有抗碰撞性,需要大量的时间才能够找到不同的输入得到相同的输出结果。也是不可逆的。使用hash
计算原文存在丢失,一个MD5
可以对应多个原文,即有限的MD5与无限的原文,一个MD5有128bit
二进制,有2^128
中可能。
彩虹表
可以根据彩虹表破解,相当于已知到一堆字符串的md5值是什么,然后将这些存储起来反向查询。但是多次md5,也是不安全的,因为我们在彩虹表会存一堆字符串2次md5值,3次md5值等。 这里就有人会问到什么是彩虹表,摘抄维基百科: 它是一个用于加密散列函数逆运算预先计算好的表。常用于破解加密后的密码散列。查找表包含有限字符固定长度的纯文本密码,是一种空间换时间实践,在暴力破解中,使用更多的存储空间与较少的计算能力,但比每一次输入散列查找表使用更少的储存空间与更多的计算能力
可以通过以下方法增加破解难度:
- 由于
sha1,sha256,sha512
也是类似于md5
,所以可以通过,md5后再sha1等增加“一点”安全性,减少彩虹表破解可能性 - 真正公认的方法是
md5
或sha1加“盐”,就是要进行md5的字符串,加上个随机字符串,再进行md5加密,这个随机字符串存储在该用户的字段中
盐
这里就涉及一个新的名词,盐。那什么是盐呢。
- 在密码学中,是指在散列之前,将散列内容任意固定位置插入特定的字符串,这种插入字符串的方式称为加盐,在大部分情况,盐不需要保密,盐可以是随机字符串,也可以是随机位置,这样安全性就大大提高。 加盐好处:
- 通常情况,当字段通过MD5加密,散列后的值是无法通过算法获取原始值,但是在一个大型的彩虹表中,通过在表中搜多该MD5值,有可能短时间获取散列值。但是加盐后的散列值,即使通过彩虹表获取散列后的数值对应的原始内容,但是加盐后插入的字符串扰乱了真正的密码,是的获取真正密码的概率大大降低。
MD5算法特点
- 压缩性,任意长度的数据,算出MD5的值都是固定的
- 容易计算,从原始数据计算出MD5值很容易
- 抗修改性,对原数据进行任何改动,哪怕只修改1个字节,最后得到的MD5值区别都很大
- 强抗碰撞:想找到两个不同数据,使它们MD5值相同非常困难
MD5用途
- 文件校验,对文件进行MD5校验,就能得到文件在传输过程中有没有被篡改
- 密码加密
MD5加密方法
- 初始化
MessageDigest
对象 - 传入需要计算的字符串,先使用
getBytes
方法生成字符串数据,在调用update
方法进行更新摘要 - 计算摘要值,调用
MessageDisgest
对象的disgest
方法完成计算,计算结果通过字节类型返回 - 处理计算结果
普遍使用加密方式有:位运算,格式化字符串,使用算法将加密后的数据转换成16进制
代码:
代码语言:javascript复制public class MD5Util {
public final static String getMD5String(String s) {
char hexDigits[] = { '0', '1', '2', '3', '4',
'5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F' };
try {
byte[] btInput = s.getBytes();
//获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
//使用指定的字节更新摘要
mdInst.update(btInput);
//获得密文
byte[] md = mdInst.digest();
//把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i ) {
byte byte0 = md[i];
str[k ] = hexDigits[byte0 >>> 4 & 0xf];
str[k ] = hexDigits[byte0 & 0xf];
}
return new String(str);
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
MD5与Base64结合使用
base64
是将任意序列的8位字节描述一种不易为人识别的形式,该算法是用64个字符来表示任意二进制数据的方法。通常用于邮件,http
加密,登录用户名密码加密,可以进行加密与解密,建议它只是一种编码格式,并不是一种加密算法,不要用来加密数据
MD5加密后还要使用Base64编码原因:
- 使用
Base64
算法编码后得到32位字符串长度值,有利于在数据库中进行存储
后起之秀
MD5
与SHA-1
是最常用的摘要算法,一个生成16字节一个生成20位字节长度,但是安全强度比较低,都被TLS
(传输层安全,一种安全通信协议)禁用。
目前推荐的是SHA-2。SHA-2
实际上是一系列摘要算法的统称,总共有 6 种,常用的有 SHA224、SHA256、SHA384
,分别能够生成 28 字节、32 字节、48 字节的摘要。
摘要算法保证了“数字摘要”和原文是完全等价的。所以,我们只要在原文后附上它的摘要,就能够保证数据的完整性。
对称式加密
DES与AES
- DES默认是56位加密密钥,已经不安全
- AES加密模式不要使用
ECB
模式,它不安全,所以推荐使用CBC
或CFB
模式,并且使用PKCS5Padding
进行填充。 使用CBC
模式,需要一个IV
参量,就是之前随机生成的指定长度字符串,来增强加密。
最早有 ECB、CBC、CFB、OFB
等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不怎么用了。最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data
),在加密的同时增加了认证的功能,常用的是 GCM、CCM
和 Poly1305
。
把上面这些组合起来,就可以得到 TLS 密码套件中定义的对称加密算法。比如:
AES128-GCM
,意思是密钥长度为 128 位的AES
算法,使用的分组模式是GCM;ChaCha20-Poly1305
的意思是ChaCha20
算法,使用的分组模式是Poly1305
。
在PKCS5Padding
中,明确定义Block
的大小是8位,而在PKCS7Padding
定义中,对于块的大小是不确定的,可以在1-255之间,填充值的算法都是一样的value=k - (l mod k) ,K=块大小,l=数据长度,如果l=8, 则需要填充额外的8个byte的8
而使用NoPadding
模式,要求输入的长度必须为16字节的倍数,又设置了CBC模式,还需附带一个IV
参量,增加加密算法强度,而CFB
不需要IV
参量。其他模式加密数据长度为16*(n 1)
的倍数。
代码:
代码语言:javascript复制 private final static String HEX = "0123456789ABCDEF";
private static final String CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";//AES是加密方式 CBC是工作模式 PKCS5Padding是填充模式
private static final String AES = "AES";//AES 加密
private static final String SHA1PRNG="SHA1PRNG";//// SHA1PRNG 强随机种子算法, 要区别4.2以上版本的调用方法
/*
* 生成随机数,可以当做动态的密钥 加密和解密的密钥必须一致,不然将不能解密
*/
public static String generateKey() {
try {
SecureRandom localSecureRandom = SecureRandom.getInstance(SHA1PRNG);
byte[] bytes_key = new byte[20];
localSecureRandom.nextBytes(bytes_key);
String str_key = toHex(bytes_key);
return str_key;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 对密钥进行处理
private static byte[] getRawKey(byte[] seed) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance(AES);
//for android
SecureRandom sr = null;
// 在4.2以上版本中,SecureRandom获取方式发生了改变
if (android.os.Build.VERSION.SDK_INT >= 17) {
sr = SecureRandom.getInstance(SHA1PRNG, "Crypto");
} else {
sr = SecureRandom.getInstance(SHA1PRNG);
}
// for Java
// secureRandom = SecureRandom.getInstance(SHA1PRNG);
sr.setSeed(seed);
kgen.init(128, sr); //256 bits or 128 bits,192bits
//AES中128位密钥版本有10个加密循环,192比特密钥版本有12个加密循环,256比特密钥版本则有14个加密循环。
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
return raw;
}/*
* 加密
*/
public static String encrypt(String key, String cleartext) {
if (TextUtils.isEmpty(cleartext)) {
return cleartext;
}
try {
byte[] result = encrypt(key, cleartext.getBytes());
return Base64Encoder.encode(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*
* 加密
*/
private static byte[] encrypt(String key, byte[] clear) throws Exception {
byte[] raw = getRawKey(key.getBytes());
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
/*
* 解密
*/
public static String decrypt(String key, String encrypted) {
if (TextUtils.isEmpty(encrypted)) {
return encrypted;
}
try {
byte[] enc = Base64Decoder.decodeToBytes(encrypted);
byte[] result = decrypt(key, enc);
return new String(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*
* 解密
*/
private static byte[] decrypt(String key, byte[] encrypted) throws Exception {
byte[] raw = getRawKey(key.getBytes());
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
RSA
非对称式加密。用私钥加密必须通过公钥解密,用公钥加密必须通过私钥解密 密钥不要低于512位,512位与1024位都已经被成功破解,所以建议使用2048位密钥长度,进行数字签名
android系统的RSA实现是"RSA/None/NoPadding"
,而标准JDK实现是"RSA/None/PKCS1Padding"
,这样会造成了在android机上加密后无法在服务器上解密的原因,所以得统一成JDK标准实现
RSA非对称加密内容长度有限制,1024位key的最多只能加密127位数据,否则就会报错(javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes)
, RSA 是常用的非对称加密算法。研究发现是由于待加密的数据超长所致。
RSA 算法规定:待加密的字节数不能超过密钥的长度值除以 8 再减去 11(即:KeySize / 8 - 11
)
私钥的加解密都很耗时,所以可以根据不同的需求采用不能方案来进行加解密。个人觉得服务器要求解密效率高,客户端私钥加密,服务器公钥解密比较好
RSA算法是最流行的公钥密码算法,使用长度可以变化的密钥。RSA是第一个既能用于数据加密也能用于数字签名的算法。
RSA算法原理如下:
- 随机选择两个大质数p和q,p不等于q,计算
N=pq
; - 选择一个大于1小于N的自然数e,e必须与
(p-1)(q-1)
互素 - 用公式计算出
d:d×e = 1 (mod (p-1)(q-1))
- 销毁p和q
最终得到的N和e就是“公钥”,d就是“私钥”,发送方使用N去加密数据,接收方只有使用d才能解开数据内容。
RSA的安全性依赖于大数分解,小于1024位的N已经被证明是不安全的,而且由于RSA算法进行的都是大数计算,使得RSA最快的情况也比DES慢上倍,这是RSA最大的缺陷,因此通常只能用于加密少量数据或者加密密钥,但RSA仍然不失为一种高强度的算法。
后起之秀
ECC(Elliptic Curve Cryptography
)是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE
用于密钥交换,ECDSA
用于数字签名。
目前比较常用的两个曲线是 P-256
(secp256r1
,在 OpenSS
L 称为 prime256v1
)和 x25519
。P-256
是 NIST(美国国家标准技术研究所)和 NSA(美国国家安全局)推荐使用的曲线,而 x25519
被认为是最安全、最快速的曲线。
比起 RSA
,ECC
在安全强度和性能上都有明显的优势。160 位的 ECC
相当于 1024 位的 RSA
,而 224 位的 ECC
则相当于 2048 位的 RSA
。因为密钥短,所以相应的计算量、消耗的内存和带宽也就少,加密解密的性能就上去了。
SQL 注入
SQL注入攻击指的是未将数据与代码进行严格的隔离,导致用户在读取数据时,错误将代码当做数据执行,导致一些安全问题,典型的例子是当对SQL语句进行拼接操作时,直接将未加转义的用户输入内容作为变量。
预防注入:
- 过滤用户输入的参数中的特殊字符,降低注入风险
- 禁止通过字符串拼接SQL语句,严格使用参数绑定传入的SQL参数
- 合理使用数据库访问框架提供的防注入机制