哈希算法和·Hmac算法 对称式与非对称式加密对比

哈希算法( Hash )又称摘要算法( Digest ),

作用:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。 哈希算法的目的:为了验证原始数据是否被篡改。 哈希算法最重要的特点就是: 相同的输入一定得到相同的输出; 不同的输入大概率得到不同的输出。

 Java字符串的 hashCode() 就是一个哈希算法,它的输入是任意字符串,输出是固定的 4 字节 int 整数

"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f

两个相同的字符串永远会计算出相同的 hashCode ,否则基于 hashCode 定位的 HashMap 就无法正常工作。这也是为什么当我们自定义 一个 class 时,覆写 equals() 方法时我们必须正确覆写 hashCode() 方法。

哈希冲突:两个不同的内容却又相同的哈希值:

"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0

"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03

 为了避免碰撞我们输出的长度越长越好:

Java 标准库提供了常用的哈希算法,并且有一套统一的接口。我们以 MD5 算法为例,看看如何对输入计算哈希:

import java.security.MessageDigest;

public class main {
	public static void main(String[] args)  {
		// 创建一个MessageDigest实例:
        MessageDigest md = MessageDigest.getInstance("MD5");
       
        // 反复调用update输入数据:
        md.update("Hello".getBytes("UTF-8"));
        md.update("World".getBytes("UTF-8"));
        
        // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
        byte[] results = md.digest(); 

        StringBuilder sb = new StringBuilder();
        for(byte bite : results) {
        	sb.append(String.format("%02x", bite));
        }
        
        System.out.println(sb.toString());
	}
}

使用 MessageDigest 时,我们首先根据哈希算法获取一个 MessageDigest 实例,然后,反复调用 update(byte[]) 输入数据。当输入 结束后,调用 digest() 方法获得 byte[] 数组表示的摘要,最后,把它转换为十六进制的字符串。 运行上述代码,可以得到输入 HelloWorld 的 MD5 是 68e109f0f40ca72a15e05cc22786f8e6 。

MD5:

可以校验下载文件是否为原本文件;

可以存储数据库的密码,这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须 用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的 MD5 恰好等于指定值。 使用哈希口令时,还要注意防止彩虹表攻击。什么是彩虹表呢?上面讲到了,如果只拿到 MD5 ,从 MD5 反推明文口令,只能使 用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的 MD5 的 对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从 MD5 一下就能反查到原始口令

所以我们可以进行添加操作:使用SHA-1 也是一种哈希算法,它的输出是 160 bits ,即 20 字节。 SHA-1 是由美国国家安全局开发的, SHA 算法实际上是一个系列,包括 SH A-0 (已废弃)、 SHA-1 、 SHA-256 、 SHA-512 等。

import java.security.MessageDigest;

public class main {
	public static void main(String[] args)  {
		// 创建一个MessageDigest实例:
        MessageDigest md = MessageDigest.getInstance("SHA-1");
       
        // 反复调用update输入数据:
        md.update("Hello".getBytes("UTF-8"));
        md.update("World".getBytes("UTF-8"));
        
        // 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2
        byte[] results = md.digest(); 

        StringBuilder sb = new StringBuilder();
        for(byte bite : results) {
        	sb.append(String.format("%02x", bite));
        }
        
        System.out.println(sb.toString());
	}
}

类似的,计算 SHA-256 ,我们需要传入名称" SHA-256 ",计算 SHA-512 ,我们需要传入名称" SHA-512 "。 

常见的哈希算法:

MD5:  输出长度16个字节128位

SHA-1:输出长度20个字节160位

RipeMD-160:输出长度字20节160位

SHA-256:输出长度32个字节256位

SHA-512:输出长度64字节512位

Hmac算法:(密钥算法);

在前面讲到哈希算法时,我们说,存储用户的哈希口令时,要加盐存储,目的就在于抵御彩虹表攻击。我们回顾一下哈希算法: d igest = hash(input)

正是因为相同的输入会产生相同的输出,我们加盐的目的就在于,使得输入有所变化: digest = hash(salt + input) 这个 salt 可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同 时提供“认证码”。

Hmac 算法就是一种基于密钥的消息认证码算法,它的全称是 Hash-based Message Authentication Code ,是一种更安全的 消息摘要算法。

Hmac 算法总是和某种哈希算法配合起来用的。例如,我们使用 MD5 算法,对应的就是 Hmac MD5 算法,它相当于“加盐”的 MD 5 : HmacMD5 ≈ md5(secure_random_key, input)

因此, HmacMD5 可以看作带有一个安全的 key 的 MD5 。

使用 HmacMD5 而不是用 MD5 加 salt ,有如下好处:

HmacMD5 使用的 key 长度是 64 字节,更安全;

Hmac 是标准算法,同样适用于 SHA-1 等其他哈希算法;

Hmac 输出和原有的哈希算法长度一致。

可见, Hmac 本质上就是把 key 混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供 key 。为了保证安全,我们 不会自己指定 key ,而是通过 Java 标准库的 KeyGenerator 生成一个安全的随机的 key 。 下面是使用 HmacMD5 的参考代码:

package com.liubatian;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;

public class woker06 {
public static void main(String[] args) throws InvalidKeyException {
	String password = "ananca";
	try {
		//生成密钥
		//密钥生成器
		KeyGenerator kergeb = KeyGenerator.getInstance("HmacMD5");
		//生成密钥
		SecretKey key = kergeb.generateKey();
		
		//获取密钥
		byte [] ket = key.getEncoded();
		System.out.println("密钥长度:"+ ket.length);
		StringBuilder ke = new StringBuilder();
		for(byte j : ket) {
			ke.append(String.format("%02x", j));
		}
		System.out.println(ke);
			//使用密钥进行加密
			//获取算法对象
			Mac mac = Mac.getInstance("HmacMD5");
			//初始化密钥
			mac.init(key);
			//更新原始内容
			mac.update(password.getBytes());
			//加密
			byte [] resulyarray = mac.doFinal();
			System.out.println("加密结果:"+ resulyarray.length+" 字节");
             StringBuilder result =new StringBuilder();
             for(byte a : resulyarray) {
            	 result.append(String.format("%02x",a));
             }
         
             System.out.println("加密结果"+result);
             //System.out.println("加密结果长度"+result.length());
				
		
	} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
	}
}
}

和 MD5 相比,使用 HmacMD5 的步骤是:

1 通过名称 HmacMD5 获取 KeyGenerator 实例;

2 通过 KeyGenerator 创建一个 SecretKey 实例;

3 通过名称 HmacMD5 获取 Mac 实例;

4 用 SecretKey 初始化Mac实例;

5 对 Mac 实例反复调用 update(byte[]) 输入数据;

6 调用 Mac 实例的 doFinal() 获取最终的哈希值。 

SecretKey 不能从 KeyGenerator 生成,而是从一个 byte[] 数组恢复:

package com.liubatian;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class woker07 {
public static void main(String[] args) throws InvalidKeyException {
	String password = "ananca";
	try {
		//回复密钥
		byte [] kergeb = {126, 49, 110, 126, -79, -5, 66, 34, -122, 123, 107, -63, 106, 100, -28, 67, 19, 23, 1, 23, 47, 63, 47, 109, 123, -111, -27, -121, 103, -11, 106, -26, 110, -27, 107, 40, 19, -8, 57, 20, -46, -98, -82, 102, -104, 96, 87, -16, 93, -107, 25, -56, -113, 12, -49, 96, 6, -78, -31, -17, 100, 19, -61, -58};
		//生成密钥
		SecretKey key = new SecretKeySpec(kergeb, "HmacMD5");
		
		   //加密
			
			Mac mac = Mac.getInstance("HmacMD5");
			//初始化密钥
			mac.init(key);
			//更新原始内容
			mac.update(password.getBytes());
			//加密
			byte [] resulyarray = mac.doFinal();
			System.out.println("加密结果:"+ resulyarray.length+" 字节");
             StringBuilder result =new StringBuilder();
             for(byte a : resulyarray) {
            	 result.append(String.format("%02x",a));
             }
         
             System.out.println("加密结果"+result);
             //System.out.println("加密结果长度"+result.length());
				
		
	} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
	}
}
}

恢复 SecretKey 的语句就是 new SecretKeySpec(hkey, "HmacMD5") 。

 对称加密算法:AES加密

常见的AES加密算法:

AES:密钥长度为128,192,256字节;工作模式CBC,EBC,PCBC;填充模式NoPadding/PKCS5Padding/PKCS7Padding

IDEA:密钥长度168字节;工作模式,EBC。填充模式:PKCS5Padding/PKCS7Padding/

DES:密钥长度54,68,字节;工作模式CBC,EBC,PCBC。填充模式:NoPadding/PKCS5Padding/PKCS7Padding

 ECB:模式:

import java.security.*;
import java.util.Base64;

import javax.crypto.*;
import javax.crypto.spec.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // 原文:
        String message = "Hello, world!";
        System.out.println("Message(原始信息): " + message);
        
        // 128位密钥 = 16 bytes Key:
        byte[] key = "1234567890abcdef".getBytes();
        
        // 加密:
        byte[] data = message.getBytes();
        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));
    }

    // 加密:
    public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
    	// 创建密码对象,需要传入算法/工作模式/填充模式
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    	
        // 根据key的字节内容,"恢复"秘钥对象
        SecretKey keySpec = new SecretKeySpec(key, "AES");
        
        // 初始化秘钥:设置加密模式ENCRYPT_MODE
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        
        // 根据原始内容(字节),进行加密
        return cipher.doFinal(input);
    }

    // 解密:
    public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
    	// 创建密码对象,需要传入算法/工作模式/填充模式
    	Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        
    	// 根据key的字节内容,"恢复"秘钥对象
        SecretKey keySpec = new SecretKeySpec(key, "AES");
        
        // 初始化秘钥:设置解密模式DECRYPT_MODE
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        
        // 根据原始内容(字节),进行解密
        return cipher.doFinal(input);
    }
}

1 根据算法名称/工作模式/填充模式获取 Cipher 实例;

2 根据算法名称初始化一个 SecretKey 实例,密钥必须是指定长度

3 使用 SerectKey 初始化 Cipher 实例,并设置加密或解密模式;

4 传入明文或密文,获得密文或明文。 

 CBC模式:

package com.apesource.demo04;

import java.security.*;
import java.util.Base64;

import javax.crypto.*;
import javax.crypto.spec.*;

public class Main {
	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();
        
        // 加密:
        byte[] data = message.getBytes();
        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));
    }

    // 加密:
    public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
        // 设置算法/工作模式CBC/填充
    	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); // 生成16个字节的随机数
        System.out.println(Arrays.toString(iv));
        IvParameterSpec ivps = new IvParameterSpec(iv); // 随机数封装成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); // IV
        System.arraycopy(input, 16, data, 0, data.length); //密文
        System.out.println(Arrays.toString(iv));
        
        // 解密:
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 密码对象
        SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); // 恢复秘钥
        IvParameterSpec ivps = new IvParameterSpec(iv); // 恢复IV
        
        // 初始化秘钥:操作模式、秘钥、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;
    }
}

对称加密算法使用同一个密钥进行加密和解密,常用算法有 DES 、 AES 和 IDEA 等; 密钥长度由算法设计决定, AES 的密钥长度是 128 / 192 / 256 位; 使用对称加密算法需要指定算法名称、工作模式和填充模式。

非对称加密: 

简单来说就是一个密钥对;一个人有一个公钥和私钥;他将公钥公开;所有人用公钥加密将信息发给这个人,这些信息就只能用这个人的私钥解密;非常安全不会泄露:

使用RSA算法实现:

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[] 数组恢复公钥或私 钥,可以这么写:

非对称加密就是加密和解密使用的不是相同的密钥,只有同一个公钥-私钥对才能正常加解密;

你可能感兴趣的:(哈希算法,算法)