哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。哈希算法的目的:为了验证原始数据是否被篡改。
哈希算法最重要的特点就是:
●相同的输入一定得到相同的输出;
●不同的输入大概率得到不同的输出。
Java字符串的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定的4字节int整数:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
哈希碰撞
哈希碰撞是指,两个不同的输入得到了相同的输出:
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0
"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
哈希碰撞是我们不可避免的,因为输出的字节长度是固定的,String的hashCode()输出是4字节整数,最多只有4294967296种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
常用哈希算法
因为我们需要控制加密后的长度一致,所以采用拼接字符串的方法,然后使用String.format("%02x",byte)方法与格式来控制每一位字节转为字符时都是两位,不足位用0补齐
常用的哈希算法有:根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
算法 | 输出长度(位) | 输出长度(字节) |
MD5 | 128 bits | 16 bytes |
SHA-1 | 160 bits | 20 bytes |
RipeMD-160 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
SHA-512 | 512 bits | 64 bytes |
MD5
Java标准库提供了常用的哈希算法,并且有一套统一的接口。我们以MD5算法为例,看看如何对输入计算哈希:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Test01 {
public static void main(String[] args) {
try {
// 获取基于MD5加密算法的工具对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 更新原始数据
md5.update("Hello".getBytes());
md5.update("World".getBytes());
// 加密后的结果
byte[] buff = md5.digest();
System.out.println(Arrays.toString(buff));
// 只要内容相同,加密的结果也相同
MessageDigest tempmd5 = MessageDigest.getInstance("MD5");
tempmd5.update("HelloWorld".getBytes());
buff = tempmd5.digest();
System.out.println(Arrays.toString(buff));
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
SHA-1
public class Test04 {
public static void main(String[] args) {
String password = "wbjxxmy";
// String salt = UUID.randomUUID().toString().substring(0,5);
String salt ="d4887";
System.out.println(salt);
// 获取SHA-1算法的工具对象
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
// 加"盐"(防止彩虹表攻击)
digest.update((password+salt).getBytes());
byte[] buff = digest.digest();
StringBuilder sb = new StringBuilder();
for(byte s : buff) {
// 设置String格式,以确保每位字节转为两位字符,不足两位用0补齐
sb.append(String.format("%02x", s));
}
System.out.println(sb);
System.out.println(sb.length());
System.out.println();
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// MD5
try {
MessageDigest digestMD5 = MessageDigest.getInstance("MD5");
digestMD5.update((password+salt).getBytes());
byte[] buff = digestMD5.digest();
StringBuilder sb = new StringBuilder();
for(byte s : buff) {
sb.append(String.format("%02x", s));
}
System.out.println(sb);
System.out.println(sb.length());
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
我们可以给MD5和SHA-1等hash算法加盐,以防止彩虹表的攻击也就是给对每个口令额外添加一个随机数或字符串等等
Hmac
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是Hmac MD5算法,它相当于“加盐”的MD5:HmacMD5 ≈ md5(secure_random_key, input)。HmacMD5使用的key长度是64字节,更安全;也同样适用于SHA-1算法等其它算法
与MD5有所不同的是,我们先使用密钥生成器类KeyGenerator获取当前算法的密钥,然后再使用SecretKey类生成密钥,最后再使用Mac类将生成的密钥加入到需要加密的字符串内,而将加密后的内容转为byte[]数组的方法是doFinal()。代码如下:
String str = "一寸相思成万缕";
try {
// 1.生成密钥
// 密钥生成器KeyGenerator
KeyGenerator ketGen = KeyGenerator.getInstance("HmacMD5");
// 生成密钥key
SecretKey key = ketGen.generateKey();
// 获取密钥key的字节数组(64)
byte[] buff = key.getEncoded();
System.out.println(Arrays.toString(buff));
System.out.println("密钥长度"+buff.length);
StringBuilder sb = new StringBuilder();
for(byte b:buff) {
sb.append(String.format("%02x", b));
}
System.out.println("密钥结果"+sb);
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(str.getBytes());
buff = mac.doFinal();
System.out.println("加密结果:"+buff.length+"字节");
StringBuilder resultSb = new StringBuilder();
for(byte b:buff) {
resultSb.append(String.format("%02x", b));
}
System.out.println("加密结果:"+resultSb);
System.out.println("加密结果长度:"+resultSb.length());
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvalidKeyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?这时,SecretKey不能从KeyGenerator生成,而是从一个byte[]数组恢复:这里值得一提的是字节数组里存的是密钥的字节内容所以想要验证需要保存密钥的字节数组。
public class Test06 {
public static void main(String[] args) {
String str = "一寸相思成万缕";
try {
// 通过"密钥字节数组""恢复密钥
byte[] buff = { -89, 2, 59, -50, -45, 75, -13, -22, 104, 8, -125, 83, 105, -60, -40, -18, 8, 6, -7, -40, 116,
68, 83, -99, -25, 2, -110, -48, -73, 39, -33, -55, -120, 21, -121, -28, -2, -14, -41, 79, 125, -23,
-105, 77, -54, 43, 70, -57, -36, -6, 73, -55, 123, 70, -59, -118, -47, -40, 39, -125, -27, 37, 112,
-113 };
SecretKey key = new SecretKeySpec(buff, "HmacMD5");
// 加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(str.getBytes());
byte[] result = mac.doFinal();
StringBuilder resultSb = new StringBuilder();
for(byte b:result) {
resultSb.append(String.format("%02x", b));
}
System.out.println(resultSb);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
BouncyCastle
BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法。
我们先需要首先,我们必须把BouncyCastle提供的bcprov-jdk15on-1.70.jar添加至classpath。这个jar包可以从官方网站下载,也可以点击下方链接。
其次,Java标准库的java.security包提供了一种标准机制,允许第三方提供商无缝接入。我们要使用BouncyCastle提供的RipeMD160算法,需要先把BouncyCastle注册一下:
public class Test07 {
public static void main(String[] args) {
try {
// 注册BouncyCastle提供的通知类对象BouncyCastleProvider
Security.addProvider(new BouncyCastleProvider());
// 获取RipeMD160算法的"消息摘要对象"(加密对象)
MessageDigest digest = MessageDigest.getInstance("RipeMD160");
// 更新原始数据
digest.update("HelloWorld".getBytes());
// 获取消息摘要(加密)
byte[] result = digest.digest();
System.out.println(result.length); // 160位 = 20字节
System.out.println(Arrays.toString(result));
// 16进制内容字符串
String hex = new BigInteger(1,result).toString(16);
System.out.println(hex.length()); // 20字节 = 40个字符
System.out.println(hex);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
对称加密算法
对称加密算法是指可以将加密后的内容还原回去
在软件开发中,常用的对称加密算法有:
算法 | 密钥长度 | 工作模式 | 填充模式 |
DES | 56/64 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/... |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA | 128 | ECB | PKCS5Padding/PKCS7Padding/... |
最后注意,DES算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了。
使用AES加密(此算法不用控制位数)
AES算法是目前应用最广泛的加密算法。比较常见的工作模式是ECB和CBC
ECB模式
public class Test08 {
public static void main(String[] args) throws GeneralSecurityException {
String message = "wzhdxtx";
// 密钥长度需为十六位
byte[] key = "1234567890abcdef".getBytes();
byte[] data = message.getBytes();
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted(加密内容)" + Base64.getEncoder().encodeToString(encrypted));
byte[] decrypted = decrpt(key, encrypted);
System.out.println("Decrypted(解密内容)" + new String(decrypted));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
// 创建密码对象,需要传入算法/工作模式/填充模式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// 根据key的字节内容:"恢复"密钥对象
SecretKey keySpec = new SecretKeySpec(key, "AES");
// 初始化密钥:设置加密模式Cipher.ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
// 根据原始内容(字节),进行加密
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrpt(byte[] key, byte[] input) throws GeneralSecurityException {
// 创建密码对象,需要传入算法/工作模式/填充模式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// 根据key的字节内容:"恢复"密钥对象
SecretKey keySpec = new SecretKeySpec(key, "AES");
// 初始化密钥:设置解密模式Cipher.DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, keySpec);
// 根据原始内容(字节),进行解密
return cipher.doFinal(input);
}
}
CBC模式
CBC模式与ECB模式不同的是加入了(盐)这里叫iv需要IvParameterSpec来生成一个iv对象将iv掺入需要加密的内容中;同样解密时也需将刚刚生成的iv传入进去才能解密。
public class Test09 {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 256位密钥 = 32 bytes Key:
byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// CBC模式需要生成一个16 bytes的initialization vector:
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] iv = sr.generateSeed(16);
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);
byte[] data = cipher.doFinal(input);
// IV不需要保密,把IV和密文一起返回:
return join(iv, data);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
// 把input分割成IV和密文:
byte[] iv = new byte[16];
byte[] data = new byte[input.length - 16];
System.arraycopy(input, 0, iv, 0, 16);
System.arraycopy(input, 16, data, 0, data.length);
// 解密:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
return cipher.doFinal(data);
}
public static byte[] join(byte[] bs1, byte[] bs2) {
byte[] r = new byte[bs1.length + bs2.length];
System.arraycopy(bs1, 0, r, 0, bs1.length);
System.arraycopy(bs2, 0, r, bs1.length, bs2.length);
return r;
}
}
密钥交换算法 (DH算法)
此算法是双方在不传输真正的密钥的情况下使双方都能解密传输的文件内容
具体实现是需要使用KeyPairGenerator类生成一对密钥:PublicKey(公钥),PrivateKey(私钥),然后将各自的公钥给对方传过去,然后各自根据自己的私钥以及对方传入过来的公钥算出真正的密钥(共享公钥),也就是通过DH算法来实现算共享公钥的过程
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.KeyAgreement;
public class Main04 {
public static void main(String[] args) {
// Bob和Alice:
Person bob = new Person("Bob");
Person alice = new Person("Alice");
// 各自生成KeyPair: 公钥+私钥
bob.generateKeyPair();
alice.generateKeyPair();
// 双方交换各自的PublicKey(公钥):
// Bob根据Alice的PublicKey生成自己的本地密钥(共享公钥):
bob.generateSecretKey(alice.publicKey.getEncoded());
// Alice根据Bob的PublicKey生成自己的本地密钥(共享公钥):
alice.generateSecretKey(bob.publicKey.getEncoded());
// 检查双方的本地密钥是否相同:
bob.printKeys();
alice.printKeys();
// 双方的SecretKey相同,后续通信将使用SecretKey作为密钥进行AES加解密...
}
}
// 用户类
class Person {
public final String name; // 姓名
// 密钥
public PublicKey publicKey; // 公钥
private PrivateKey privateKey; // 私钥
private byte[] secretKey; // 本地秘钥(共享密钥)
// 构造方法
public Person(String name) {
this.name = name;
}
// 生成本地KeyPair:(公钥+私钥)
public void generateKeyPair() {
try {
// 创建DH算法的“秘钥对”生成器
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DH");
kpGen.initialize(512);
// 生成一个"密钥对"
KeyPair kp = kpGen.generateKeyPair();
this.privateKey = kp.getPrivate(); // 私钥
this.publicKey = kp.getPublic(); // 公钥
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
// 按照 "对方的公钥" => 生成"共享密钥"
public void generateSecretKey(byte[] receivedPubKeyBytes) {
try {
// 从byte[]恢复PublicKey:
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(receivedPubKeyBytes);
// 根据DH算法获取KeyFactory
KeyFactory kf = KeyFactory.getInstance("DH");
// 通过KeyFactory创建公钥
PublicKey receivedPublicKey = kf.generatePublic(keySpec);
// 生成本地密钥(共享公钥)
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(this.privateKey); // 初始化"自己的PrivateKey"
keyAgreement.doPhase(receivedPublicKey, true); // 根据"对方的PublicKey"
// 生成SecretKey本地密钥(共享公钥)
this.secretKey = keyAgreement.generateSecret();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void printKeys() {
System.out.printf("Name: %s\n", this.name);
System.out.printf("Private key: %x\n", new BigInteger(1, this.privateKey.getEncoded()));
System.out.printf("Public key: %x\n", new BigInteger(1, this.publicKey.getEncoded()));
System.out.printf("Secret key: %x\n", new BigInteger(1, this.secretKey));
}
}
非对称加密算法
非对称加密算法就是双方各自生成一对密钥KeyPairGenerator类生成一对密钥:PublicKey(公钥),PrivateKey(私钥),然后各自以对方传入过来的公钥来加密传输,解密则是根据自己的密钥来解密。
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import javax.crypto.Cipher;
// RSA
public class Main {
public static void main(String[] args) throws Exception {
// 明文:
byte[] plain = "Hello, encrypt use RSA".getBytes("UTF-8");
// 创建公钥/私钥对:
Human alice = new Human("Alice");
// 用Alice的公钥加密:
// 获取Alice的公钥,并输出
byte[] pk = alice.getPublicKey();
System.out.println(String.format("public key(公钥): %x", new BigInteger(1, pk)));
// 使用公钥加密
byte[] encrypted = alice.encrypt(plain);
System.out.println(String.format("encrypted(加密): %x", new BigInteger(1, encrypted)));
// 用Alice的私钥解密:
// 获取Alice的私钥,并输出
byte[] sk = alice.getPrivateKey();
System.out.println(String.format("private key(私钥): %x", new BigInteger(1, sk)));
// 使用私钥解密
byte[] decrypted = alice.decrypt(encrypted);
System.out.println("decrypted(解密): " + new String(decrypted, "UTF-8"));
}
}
// 用户类
class Human {
// 姓名
String name;
// 私钥:
PrivateKey sk;
// 公钥:
PublicKey pk;
// 构造方法
public Human(String name) throws GeneralSecurityException {
// 初始化姓名
this.name = name;
// 生成公钥/私钥对:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
// 把私钥导出为字节
public byte[] getPrivateKey() {
return this.sk.getEncoded();
}
// 把公钥导出为字节
public byte[] getPublicKey() {
return this.pk.getEncoded();
}
// 用公钥加密:
public byte[] encrypt(byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, this.pk); // 使用公钥进行初始化
return cipher.doFinal(message);
}
// 用私钥解密:
public byte[] decrypt(byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, this.sk); // 使用私钥进行初始化
return cipher.doFinal(input);
}
}
RSA的公钥和私钥都可以通过getEncoded()方法获得以byte[]表示的二进制数据,并根据需要保存到文件中。要从byte[]数组恢复公钥或私钥,可以这么写:
byte[] pkData = ...
byte[] skData = ...
KeyFactory kf = KeyFactory.getInstance("RSA");
// 恢复公钥:
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pkData);
PublicKey pk = kf.generatePublic(pkSpec);
// 恢复私钥:
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(skData);
PrivateKey sk = kf.generatePrivate(skSpec);
以上就是对常见hash算法的概述