MD5加密算法

在这里插入图片描述

前言

        最近因为yq,博主在家就又学习了下MD5的加密算法!说到MD5,相信很多人都听说和使用过,因为MD5实在是太常见,经常被用做登录密码加密、文件校验等等方面

Part 1——MD5介绍

什么是MD5?

        MD5 (Message-Digest Algorithm):信息摘要算法。一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值,用于确保信息传输完整一致。MD5由美国密码学家 罗纳德·李维斯特 设计,于1992年公开。 这套算法的程序在 RFC 1321 标准中被加以规范。

MD5性质

MD5的性质如下:
(1)压缩性:无论输入多少字节数据,输出都是长度为128位(16字节)
(2)容易计算性:从原数据计算出MD5值很容易
(3)抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别
(4)抗碰撞性:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)十分困难;(注意:这里的抗碰撞性并不是说散列算法无碰撞
(5)不可逆特性:单向密码体制,从明文到密文的不可逆映射,只有加密过程没有解密过程

MD5为什么不可逆?

      我们知道MD5算法本质是一种散列函数,使用的是hash算法,在计算过程中原文的部分信息是丢失了的。至于在计算过程中原文部分信息是丢失,是因为MD5的压缩性所致,无论输入多少字节数据,输出都是长度为16字节 ,故而MD5的运算过程存在信息丢失。由于不知道运算过程中会有多少个进位在哪一步被丢弃,因而仅仅根据MD5的计算过程和得到的最终结果,是无法逆向计算出明文的。

这儿可能又有同学要问,即然不能解密,为什么网上有MD5的解密网站?

       针对这个问题,首先我们得知道这里所说的解密是不是真正的解密,答案很显然不是。网上的md5解密网站是成千上万的md5原文和md5密文,放到了数据里,所谓的解密就是从数据库里查询有没有原文。这种网站相当于md5的字典库,就是原文和密文的的对应表,数据量很庞大,上万亿级别,如果用户的密文正好在字典库里面,一查对应表就行。很多用户的密码都不够复杂,所以很容易被这种方式生成出来。一般网上这种md5解密网站能解密8位数左右的纯数字密码。密码太复杂的话,要根据这个网站的数据库和数据量而定。

MD5碰撞(Collision)

        2004年,我国中科院院士王小云证实md5算法无法防止碰撞(collision),因此不适用于安全性认证。实际上,王小云的研究成果如下:MD5( M1)=MD5( M2) 即给定消息M1,能够计算获取M2,使得M2产生的散列值与M1产生的散列值相同。如此,MD5的抗碰撞性就已经不满足了,使得MD5不再是安全的散列算法。但即便如此,直到今天,MD5算法依旧没有被彻底抛弃。

MD5碰撞概率

        简单来说,就是先得出一个字符串的MD5值,在根据这个值,逆算出另外一个不同的字符串,但是它们的MD5值是一致的。 这就是MD5碰撞。MD5碰撞概率为 1 / 2 256 1/2^{256} 1/2256,从这个概率可以看出MD5发生碰撞的概率还是极低的,几乎不可能。
        那我们有办法实现MD5碰撞的实现吗?答案是显然的,上面我们提到我国中科院院士王小云证实md5算法无法防止碰撞(collision),那我们当然可以通过一些技术手段得到MD5的碰撞。下面博主就演示下MD5的碰撞:

1、准备两个内容不同的文本
文本1:
文本1
文本2:
文本2
2、Java MD5加密代码:

import java.io.File;
import java.io.FileInputStream;
import java.security.MessageDigest;

public class md5ToFile {
	public static void main(String[] args) {
		System.out.println("=====MD5碰撞演示======");
		getFile("1_msg1.txt");
		getFile("1_msg2.txt");
	}

	// MD5加密文件
	private static void getFile(String path) {
		File file = new File(path);// 文件路径
		String md5 = getFileMD5(file);// 得到MD5码
		System.out.println(file.getName()+"加密文件的MD5为:\t" + md5);
	}

	public static String getFileMD5(File file) {
		if (!file.isFile()) {
			return null;
		}
		MessageDigest md = null;
		FileInputStream in = null;
		byte buffer[] = new byte[1024];
		int len;
		try {
			md = MessageDigest.getInstance("MD5");
			in = new FileInputStream(file);
			while ((len = in.read(buffer, 0, 1024)) != -1) {
				md.update(buffer, 0, len);
			}
			in.close();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
		return bytesToHexString(md.digest());
	}

	public static String bytesToHexString(byte[] md5Arr) {
		StringBuilder stringBuilder = new StringBuilder("");
		if (md5Arr == null || md5Arr.length <= 0) {
			return null;
		}
		//把byte[]数组转换成十六进制字符串表示形式
		for (int i = 0; i < md5Arr.length; i++) {
			//进行按位与操作,保证byte类型转int后其保持二进制补码的一致
			int v = md5Arr[i] & 0xFF;
			String md5Hex = Integer.toHexString(v);
			stringBuilder.append(md5Hex);
		}
		return stringBuilder.toString();
	}
}

加密结果:

在这里插入图片描述
我们可以看见,这两个文本的内容是不一样的,但经过MD5加密后,MD5确实一样的,说明MD5还是能产生碰撞,因此密码安全性要求很高的,不建议采用MD5加密。

Part 2——MD5加密原理

1、数据填充

在这里插入图片描述

2、添加消息长度

在这里插入图片描述

3、装入标准幻数

在这里插入图片描述

4、四轮循环运算

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实现代码

public class MD5Source {
    //分块存储数组
    long[] groups = null;
    //MD5存储结果
    String resultMessage = "";

    //定义标准幻数,采用小端存储
    static final long A = 0x67452301L;
    static final long B = 0xefcdab89L;
    static final long C = 0x98badcfeL;
    static final long D = 0x10325476L;

    //基本数据
    private long[] result = {A, B, C, D};
    //常量ti公式:floor(abs(sin(i+1))×(2pow32)
    static final long T[][] = {
            {0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
                    0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
                    0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
                    0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821},

            {0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
                    0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
                    0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
                    0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a},

            {0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
                    0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
                    0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
                    0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665},

            {0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
                    0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
                    0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
                    0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}};
    //向左位移数
    static final int k[][] = {
            {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
            {1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12},
            {5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2},
            {0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9}};

    //各次迭代中采用的做循环移位的s值
    static final int S[][] = {
            {7, 12, 17, 22},
            {5, 9, 14, 20},
            {4, 11, 16, 23},
            {6, 10, 15, 21}};

    //4轮循环中使用的生成函数(轮函数)g
    private static long g(int i, long b, long c, long d) {
        switch (i) {
            case 0:
                return (b & c) | ((~b) & d);
            case 1:
                return (b & d) | (c & (~d));
            case 2:
                return b ^ c ^ d;
            case 3:
                return c ^ (b | (~d));
            default:
                return 0;
        }
    }
    //开始使用MD5加密
    private String start(String message) {
        //转化为字节数组
        byte[] inputBytes = message.getBytes();
        //获取字节数组的长度
        int byteLen = inputBytes.length;
        //得到K值(以bit作单位的message长度)
        long K = (long) (byteLen << 3);

        //完整小组(512bit)(64byte)的个数
        int groupCount = byteLen / 64;

        //分块
        for (int i = 0; i < groupCount; i++) {
            //每次取512bit
            //处理一个分组
            H(divide(inputBytes, i * 64));
        }

        //填充
        int rest = byteLen % 64;
        //即将填充的一个分组
        byte[] paddingBytes = new byte[64];
        //原来的尾部数据
        for (int i = 0; i < rest; i++)
            paddingBytes[i] = inputBytes[byteLen - rest + i];
        //即小于448bit的情况,先填充100...0再填充K值的低64位
        //此时只会新增一个分组
        if (rest <= 56) {
            //填充100...0
            if (rest < 56) {
                //填充10000000
                paddingBytes[rest] = (byte) (1 << 7);
                //填充00000000
                for (int i = 1; i < 56 - rest; i++)
                    paddingBytes[rest + i] = 0;
            }
            //填充K值低64位
            for (int i = 0; i < 8; i++) {
                paddingBytes[56 + i] = (byte) (K & 0xFFL);
                K = K >> 8;
            }
            //处理分组
            H(divide(paddingBytes, 0));
            //即大于448bit的情况,先填充100...0再填充K值的低64位
            //此时会新增两个分组
        } else {
            //填充10000000(左移)
            paddingBytes[rest] = (byte) (1 << 7);
            //填充00000000
            for (int i = rest + 1; i < 64; i++)
                paddingBytes[i] = 0;
            //处理第一个尾部分组
            H(divide(paddingBytes, 0));

            //填充00000000
            for (int i = 0; i < 56; i++)
                paddingBytes[i] = 0;

            //填充低64位
            for (int i = 0; i < 8; i++) {
                //使用小端方式,即Byte数组先存储len的低位数据,然后右移len
                paddingBytes[56 + i] = (byte) (K & 0xFFL);
                K = K >> 8;
            }
            //处理第二个尾部分组
            H(divide(paddingBytes, 0));
        }
        //将Hash值转换成十六进制的字符串
        //小端方式
        for (int i = 0; i < 4; i++) {
            //解决缺少前置0的问题
            resultMessage += String.format("%02x", result[i] & 0xFF) +
                    String.format("%02x", (result[i] & 0xFF00) >> 8) +
                    String.format("%02x", (result[i] & 0xFF0000) >> 16) +
                    String.format("%02x", (result[i] & 0xFF000000) >> 24);
        }
        return resultMessage;
    }

    //从inputBytes的index开始取512位,作为新的512bit的分组
    private static long[] divide(byte[] inputBytes, int start) {
        //存储一整个分组,就是512bit,数组里每个是32bit,就是4字节,为了消除符号位的影响,所以使用long
        long[] group = new long[16];
        for (int i = 0; i < 16; i++) {
            //每个32bit由4个字节拼接而来
            //小端的从byte数组到bit恢复方法
        	//“<<”表示循环左移位
            group[i] = byte2unsign(inputBytes[4 * i + start]) |
                    (byte2unsign(inputBytes[4 * i + 1 + start])) << 8 |
                    (byte2unsign(inputBytes[4 * i + 2 + start])) << 16 |
                    (byte2unsign(inputBytes[4 * i + 3 + start])) << 24;
        }
        return group;
    }

    //其实byte相当于一个字节的有符号整数,这里不需要符号位,所以把符号位去掉
    public static long byte2unsign(byte b) {
        return b < 0 ? b & 0x7F + 128 : b;//0x7F--0111 1111
    }

    // groups[] 中每一个分组512位(64字节)
    // MD5压缩函数
    private void H(long[] groups) {
        //缓冲区数组
        long a = result[0], b = result[1], c = result[2], d = result[3];
        //四轮循环
        for (int n = 0; n < 4; n++) {
            //16轮迭代
            for (int i = 0; i < 16; i++) {
                result[0] += (g(n, result[1], result[2], result[3]) & 0xFFFFFFFFL) + groups[k[n][i]] + T[n][i];
                result[0] = result[1] + ((result[0] & 0xFFFFFFFFL) << S[n][i % 4] | ((result[0] & 0xFFFFFFFFL) >>> (32 - S[n][i % 4])));
                //循环轮换
                long temp = result[3];
                result[3] = result[2];
                result[2] = result[1];
                result[1] = result[0];
                result[0] = temp;
            }
        }
        //加入之前计算的结果
        result[0] += a;
        result[1] += b;
        result[2] += c;
        result[3] += d;
        //防止溢出
        for (int n = 0; n < 4; n++) {
            result[n] &= 0xFFFFFFFFL;
        }
    }

    public static void main(String[] args) {
        MD5Source md = new MD5Source();
        //pwd的MD5:9003d1df22eb4d3820015070385194c8
        String message = "pwd";
        System.out.println("原始数据: " + message);
        System.out.println("加密结果: " + md.start(message));
        System.out.println("加密结果(Upper): " + md.resultMessage.toUpperCase());
    }
}

Part 3——MD5加盐

1、MD5+salt原理:
        MD5算法在保存用户密码信息的过程中,加入盐。盐可以是生成的随机数,也可以是自定义的数据。将密码结合MD5加盐,生成的数据摘要和盐保存起来 。以便于下次用户验证使用。就是在用户表里面,也保存salt。
2、为什么要MD5+盐:
        我们知道如果直接对密码进行散列,那么黑客可以对通过获得这个密码散列值,然后通过查散列值字典(如MD5密码破解网站),得到某用户的密码。
  加Salt可以一定程度上解决这一问题。所谓加Salt方法,就是加点“佐料”。其基本想法是这样的:当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。
  这里的“佐料”被称作“Salt值”,这个值是如果由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。破解的几率大大减小。

实现代码

import java.util.Random;
import org.apache.commons.codec.binary.Hex;
import java.security.MessageDigest;

public class md5AddSalt {

	/**
	 * 普通MD5
	 * @param x 原文
	 * @return result 密文
	 */
	public static String MD5(String x) {
		MessageDigest m;
		String result = "";
		try {
			m = MessageDigest.getInstance("MD5");
			m.update(x.getBytes("UTF8"));
			byte s[] = m.digest();
			for (int i = 0; i < s.length; i++) {
				result += Integer.toHexString((0x000000ff & s[i]) | 0xffffff00).substring(6);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 加盐MD5
	 * @param password 原文
	 * @return String(cArr) 加盐后的密文
	 */
	public static String generate(String password) {
		Random r = new Random();
		StringBuilder sb = new StringBuilder(16);
		sb.append(r.nextInt(99999999)).append(r.nextInt(99999999));
		int len = sb.length();
		if (len < 16) {
			for (int i = 0; i < 16 - len; i++) {
				sb.append("0");
			}
		}
		String salt = sb.toString();
		//将“盐”加到明文中,并生成新的MD5码
		password = md5Hex(password + salt);
		//将“盐”混到新生成的MD5码中,之所以这样做是为了后期更方便的校验明文和秘文
		char[] cArr = new char[48];//MD5(32位)+Slat(16位)
		for (int i = 0; i < 48; i += 3) {
			cArr[i] = password.charAt(i / 3 * 2);
			char c = salt.charAt(i / 3);
			cArr[i + 1] = c;
			cArr[i + 2] = password.charAt(i / 3 * 2 + 1);
		}
		return new String(cArr);
	}

	/**
	 * 校验加盐后是否和原文一致
	 * @param password
	 * @param md5
	 * @return
	 */
	public static boolean verify(String password, String md5) {
		//先从MD5码中取出之前加的“盐”和加“盐”后生成的MD5码
		char[] cArr1 = new char[32];//MD5
		char[] cArr2 = new char[16];//Salt
		for (int i = 0; i < 48; i += 3) {
			cArr1[i / 3 * 2] = md5.charAt(i);
			cArr1[i / 3 * 2 + 1] = md5.charAt(i + 2);
			cArr2[i / 3] = md5.charAt(i + 1);
		}
		String salt = new String(cArr2);
		return md5Hex(password + salt).equals(new String(cArr1));
	}

	/*
	 * 获取十六进制字符串形式的MD5摘要
	 */
	private static String md5Hex(String src) {
		try {
			MessageDigest md5 = MessageDigest.getInstance("MD5");
			byte[] bs = md5.digest(src.getBytes());
			return new String(new Hex().encode(bs));
		} catch (Exception e) {
			return null;
		}
	}
}
  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

✎浅笑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值