MD5算法的实现详解

提起加密,很多人会将MD5也列举出来,说MD5加密,这样说其实是不严谨的,不正确的。Message Digest Algorithm MD5(中文名为消息摘要算法第五版)是一种摘要算法(首先名字里面都没有带加密的字眼),单向的,不可逆的,它的功能通常是用来验证文件或是数据的完整性。经它处理过的数据,外表给人的感觉是被“加密”成了难以识别的字符串,但是它并不能被称为加密算法,有一个通俗一点的理解方式为:加密算法肯定有对应的解密算法,但MD5没有,所以不能称为加密。接下来我们就来聊聊MD5的具体实现。

 首先MD5的实现需要一个非常重要的类——MessageDigest,其在oracle官网的解释是:MessageDigest类是一个引擎类,用以提供加密的安全消息摘要功能(如SHA-1或MD5等)。密码安全的消息摘要采用任意大小的输入(字节数组),并生成称为摘要或散列的固定大小的输出。摘要有两个属性:

1、找到两个散列成相同值的消息在计算上是不可行的。

2、摘要不可以揭示用于生成它的输入的任何内容。

 消息摘要用于生成唯一可靠的数据标识符。它们有时被称为数据的“数字指纹”。

 

RFC 1321是“MD5 报文摘要算法”的文件号,其中的解释为:MD5 报文摘要算法将任意长度的信息作为输入值,并将其换算成一个 128 位长度的”指纹信息”或”报文摘要”值来代表这个输入值,并以换算后的值作为结果。MD5 算法主要是为数字签名应用程序而设计的;在这个数字签名应用程序中,较大的文件将在加密(这里的加密过程是通过在一个密码系统下[如:RSA]的公开密钥下设置私有密钥而完成的)之前以一种安全的方式进行压缩(就是把一个任意长度的字节串变换成一定长的十六进制数字串)。 MD5算法具有以下特点:

1、压缩性:任意长度的数据,算出的MD5值长度都是固定的。

2、容易计算:从原数据计算出MD5值很容易。

3、抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。

4、强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。

我简单的说一下MD5算法验证的原理(如果有筒靴想了解更详细的内容,请自行百度,我们今天只是实现的详解):MD5验证在平常我们的生活中随处可见,比如说:APP的账号登录,银行卡的密码等等。就拿我们的银行卡密码来说,这是个人的隐私,除了自己任何人都不能得到,包括银行,那么当我们取钱时输入密码,银行是怎么知道我们的密码是对的,这正好用到我们的MD5算法。当我们开户时设置密码时,密码时被MD5进行了数据处理,然后存储到银行系统的数据库中,当我们取钱时,输入密码后,密码会被常用与之前同样的算法进行同样步骤的处理,然后,银行会拿数据库中存的处理过的密码和我们现在输入的处理过的密码进行对比,如果一模一样了,就给我们打开我们账户所有的权限,我们就可以对我们的账户为所欲为了,否则我们是任何操作的做不了的。据说银行将我们的密码进行了九次MD5算法处理,根据它的特点属性我们可以知道,一次都已经无法破解了,何况是九次,自己想象吧。

计算摘要的步骤为:

一、创建一个消息摘要实例

与所有引擎类一样,MessageDigest也是通过调用getInstance方法来获取特定类型的消息摘要算法的对象,该getInstance方法是MessageDigest类中的静态工厂方法:

static MessageDigest getInstance(String algorithm);

注意:算法名称不区分大小写。

调用者可以选择指定提供者的名称或提供者实例 ,其保证所请求的算法的实现来自指定的提供者:

// algorithm:指定算法名称; provider:指定提供者的名称或提供者实例 

① static MessageDigest getInstance(String algorithm,String provider)

② static MessageDigest getInstance(String algorithm,Provider provider)

调用getInstance返回初始化消息摘要对象。因此不需要进一步的初始化。

根据
oracle官网上的
Java Cryptography Architecture中的建议:如果不是有特殊要求必须得指定提供者,尽量不要指定提供者或者提供者实例,我们只需要了解有这种实例化的方法就行了

二、更新消息摘要对象,将数据传入

该步操作是为了将要进行摘要算法的数据提供给初始化的消息摘要对象。这是通过调用以下update()方法之一完成的:

方法一:

// 使用指定的字节更新摘要。input - 用于更新摘要的字节。
void update(byte input);

方法二:

// 使用指定的字节数组更新摘要。input - 字节数组。
void update(byte [] input);

方法三:

// 使用指定的字节数组,从指定的偏移量开始更新摘要。input - 字节数组;offset - 字节数组中的偏移量,操作从此处开始;len - 要使用的字节数。
void update(byte [] input,int offset,int len);

三、计算摘要

通过调用update方法提供数据后,将使用对以下digest方法之一的调用来计算摘要 :

方法一:

// 通过执行诸如填充之类的最终操作完成哈希计算。调用此方法后摘要被重置。
// byte [] - 存放哈希值结果的字节数组
byte [] digest();

方法二:

// 使用指定的字节数组对摘要进行最后更新,然后完成摘要计算。也就是说,此方法首先调用 update(input),向 update 方法传递 input 数组,然后调用 digest()。
// input - 在完成摘要计算前要更新的输入。
byte [] digest(byte [] input);

方法三:

// 通过执行诸如填充之类的最终操作完成哈希计算。调用此方法后摘要被重置。
// buf - 存放计算摘要的输出缓冲区; offset - 输出缓冲区中的偏移量,从此处开始存储摘要; len - 在 buf 中分配给摘要的字节数
int digest(byte [] buf,int offset,int len);

前两种方法返回计算的摘要,为字节数组类型。后一种方法将计算的摘要存储在提供的缓冲区中 buf,从offset开始,len是buf分配给摘要的字节数。该方法返回实际存储在buf中的字节数。

四、实例

好了,所有的步骤就这些了,我们现在来个例子总结一下,例子的主要内容就是:将一个任意想要处理的字符串进行MD5算法处理,然后输出处理后的字符串。

代码走起:

public static void main(String[] args) {
		
		try {
			String string = "待处理^de_数据(Note: You kan type in whatever you want!)";
			System.out.println("处理前的数据: " + string);
			// 创建一个消息摘要实例
			MessageDigest digester = MessageDigest.getInstance("MD5");
			// 更新消息摘要对象,将数据传入
			digester.update(string.getBytes());
			// 将传进来的String 变成byte数组进行摘要获取
			byte[] data = digester.digest();
			// 将获取的摘要信息 获取 出来,byte中存的默认是16进制,进行转换(在这儿不能将byte[]与String互转),转换为Android直接可以使用的String ,char
			String mData = toHex(data);
			System.out.println("处理后的数据: " + mData);
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

里面用到一个转换方法toHex(data),其代码为:

	/**
	 * 字符串/字节数组 ——> 十六进制数字的字符串
	 * 
	 * @param data	待处理的字节数组
	 * @return		其对应的十六进制字符串
	 */
	private static String toHex(byte[] data) {
		String str = null;
		if (null != data) {
			StringBuilder sBuilder = new StringBuilder();
			for(byte b: data) {
				// %2x 代表的就是 16进制的格式
				sBuilder.append(String.format("%2x", b));
			}
			str = sBuilder.toString();
			return str;
		}
		return null;
	}

输出结果:

处理前的数据: 待处理^de_数据(Note: You kan type in whatever you want!)
处理后的数据: bcf097d2d42859a5ae82a37efc93cc1a

扩展:

注意:消息摘要计算完毕后,消息摘要对象将自动复位并准备好接收新数据并计算其摘要。所有以前的状态(即:提供给update 的数据)都将丢失。当然,凡事都不是绝对的,有一些哈希实现(不要迷了这儿为什么用哈希,别忘了摘要算法的整个过程都是将我们传入的数据变为最终的hash值)可以通过克隆来支持中间哈希(也就是之前的状态不被丢失)。

1、接下来,假设我们有三个字节数组:i1,i2 和i3,形成总输入,我们要计算这个总输入的消息摘要。此摘要(或“哈希”)可以通过以下方式计算:

sha.update(i1); 
sha.update(i2); 
sha.update(i3); 
byte [] hash = sha.digest();

还可以等效为:

sha.update(i1); 
sha.update(i2); 
byte[] hash = sha.digest(i3);

消息摘要计算完毕后,消息摘要对象将自动复位并准备好接收新数据并计算其摘要。所有以前的状态(即:提供给update 的数据)都将丢失。

2、假设我们要计算几个单独的哈希值:i1;i1 and i2;i1, i2, and i3

一个办法是:

// 计算i1的哈希值
sha.update(i1); 
byte[] i1Hash = sha.clone().digest(); 
				
// 计算i1和i2的哈希值
sha.update(i2); 
byte[] i12Hash = sha.clone().digest(); 
				
// 计算i1,i2和i3的哈希值
sha.update(i3); 
byte[] i123hash = sha.digest();

该代码只有在SHA-1实现可克隆时才有用。虽然消息摘要的一些实现是可克隆的,但有些则不是。要确定是否可以进行克隆,请尝试克隆MessageDigest对象并捕获潜在的异常,代码如下所示:

try {
	// 尝试克隆它
	// 计算i1的哈希值
	sha.update(i1); 
	byte[] i1Hash = sha.clone().digest();
	 . . .
	byte[] i123hash = sha.digest();
} catch (CloneNotSupportedException cnse) {
	// 执行其他操作,如下面所示的代码
}

如果消息摘要不是可克隆的,那么另一个不那么优雅的计算中间摘要的方法是创建几个摘要。在这种情况下,必须提前知道要计算的中间摘要数量,代码如下:

MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
MessageDigest sha12 = MessageDigest.getInstance("SHA-1"); 
MessageDigest sha123 = MessageDigest.getInstance("SHA-1");
			
// 计算i1的哈希值
byte[] i1Hash = sha1.digest(i1);
			
// 计算i1和i2的哈希值
sha12.update(i1);
byte[] i12Hash = sha12.digest(i2);
			
// 计算i1,i2和i3的哈希值
sha123.update(i1);
sha123.update(i2);
byte[] i123Hash = sha123.digest(i3);

有没有觉得代码很眼熟,嘿嘿,想知道就往上找吧。

如有不足之处,还望留言指正,多谢各位了!

点赞