MD5详解_c++简易实现

一.概念

  MD5全称:消息摘要算法版本5(Message Digest Algorithm 5)。属Hash算法一类。MD5算法对输入任意长度的消息进行运行,产生一个128位的消息摘要(32位的数字字母混合码)。所以他有两个特点:

  1.不可逆性 ,就是根据 MD5 值计算不出原始数据。2.再者就是唯一性 ,根据不同原始数据会有不同的 MD5 值(毕竟2^32),大约等于3.4*10^38,然后是有很多比如压缩性,抗碰撞性等密码的一些普遍性质。再提一句,在线网站能进行破解的,是跟他现成数据库匹配的,并不算破解。

二.源码  

  下面就跟着源码出发,分别解析MD5加密算法的每一步,主要包含三个文件,主函数md5.cpp,md5.h头文件,和最后的运行函数text.cpp。

1.md5.cpp

1.1定义静态数组

#include "md5.h"
#include<iostream>

/* Define the static member of MD5. */
const byte MD5::PADDING[64] = { 0x80 };
const char MD5::HEX_NUMBERS[16] = {
  '0', '1', '2', '3',
  '4', '5', '6', '7',
  '8', '9', 'a', 'b',
  'c', 'd', 'e', 'f'
};

  定义两个静态数组,提供了在 MD5 算法中常用的常量数据,使得这些数据在整个类中共享,方便实现和维护。

  0x80 在二进制中的表示是 10000000,其中最高位是 1,其余都是 0。这个二进制模式用于在填充时添加一个 1,然后根据需要继续添加零,这样的填充方式是为了确保消息块的长度满足算法要求,同时在处理最后一个消息块时,可以正确地识别出填充的位置。

  提一句,我测了一下,0x80在字符中是一个特殊字符,十进制是128。PADDING[64]数组只有第一位是0x80,其余全是0。至于hex数组,是十进制到16进制的转换。

 1.2字符串构造数据处理对象(初始化内部信息)

  这一步是MD5的构造函数。

  我们本段直接定义的函数都在头文件实现,在此不一一描述。

MD5::MD5(const string& message) {
  finished = false;
  /* Reset number of bits. */
  count[0] = count[1] = 0;
  /* Initialization constants. */
  state[0] = 0x67452301;
  state[1] = 0xefcdab89;
  state[2] = 0x98badcfe;
  state[3] = 0x10325476;
  /* Initialization the object according to message. */
  init((const byte*)message.c_str(), message.length());
}

  finished 标志初始化为 false,用来表示MD5 算法尚未完成,并记录是否完成。

  因为 MD5 算法中涉及到的位数可能超出 32 位,所以使用了两个 32 位的整数来保存,来记录超级大的信息。count[0]count[1] 分别表示低 32 位和高 32 位。再进一步解释一下,单独的 32 位整数的范围是 0 到 2^32-1,其实每个数组的数都是32位,大约42亿,所以超过42亿之后,就需要更大的来记录了。

  初始化常数。四个32位变量初始化(这些是固定的,前人研究出来的,不用记住)。它们也被称为链接变量。

  最后调用 init 函数,该函数用于根据输入的消息初始化 MD5 对象。传递给 init 函数的参数是消息的字节数组和消息的长度。

1.3生成MD5摘要和返回MD5摘要

  以下是获得消息摘要的主要函数

const byte* MD5::getDigest() {
  if (!finished) {
    finished = true;
byte bits[8];//定义一个 8 字节大小的数组 bits,用于存储消息的比特数,8字节刚好64位 bit32 oldState[4];//用来保存当前状态的数组 bit32 oldCount[2];//用来保存当前计数器的数组。 bit32 index, padLen;// 32位的无符号整数 /* Save current state and count. */ memcpy(oldState, state, 16);//将当前状态保存到 oldState 数组中 memcpy(oldCount, count, 8);//将当前计数器保存到 oldCount 数组中
//使用 memcpy 函数将当前状态和计数器的值复制到相应的数组中。
/* Save number of bits */ encode(count, bits, 8);//将计数器的值编码到 bits 数组中,用于后续的消息填充。 /* Pad out to 56 mod 64. */ index = (bit32)((count[0] >> 3) & 0x3f);//计算的是当前消息的比特数对512取模的余数 padLen = (index < 56) ? (56 - index) : (120 - index);//来确定需要填充的位数,使得 len ≡ 448 mod 512 成立 init(PADDING, padLen);//使用填充数组 PADDING 来实际进行填充操作。填充的内容是一个字节 0x80,后面跟着若干个零,直到填充完。 /* Append length (before padding) */ init(bits, 8);//将计数器的编码值追加到填充后的消息末尾 /* Store state in digest */ encode(state, digest, 16);//将当前状态编码成最终的消息摘要,结果存储在 digest 数组中。 /* Restore current state and count. */ //恢复之前保存的状态和计数器的值 memcpy(state, oldState, 16); memcpy(count, oldCount, 8); } return digest; }

  state:MD5 的状态是由四个 32 位整数组成,分别存储在 state[0]state[1]state[2]state[3] 中。在保存当前状态时,将这四个整数的值依次存储到 oldState 数组中。再使用一个 64 位的计数器来记录消息的比特数,其中 count[0] 存储低 32 位,count[1] 存储高 32 位。在保存当前计数器时,将这两个整数的值依次存储到 oldCount 数组中。

  memcpy:  在处理一个消息块之前,会将当前的MD5算法状态和计数器的值保存到临时变量中。这包括将当前状态 state 的内容保存到 oldState 数组中,将当前计数器 count 的内容保存到 oldCount 数组中。在后续的处理中,需要修改 statecount 数组的值,但在某些步骤完成后,需要将这些值恢复到之前的状态。这种保存和恢复的机制是为了确保在处理不同的消息块时,对状态和计数器的修改不会相互干扰。

  encode:在具体的实现中,encode 函数通过对每个 bit32 进行字节级别的拆分,将其拷贝到 bits 中,保证了正确的编码。这是因为 bit32 是一个 32 位整数,而 bits 是一个字节数组,每个元素是一个字节。经过这个操作,64 位的计数器值表示成一个 8 字节的数组

  Pad out to 56 mod 64:

  核心在于,对消息进行数据填充,len ≡ 448 mod 512。使消息的长度对512取模得448,为什么选这个数,因为448=512-64,而填充完后后面还要再填上64位的原数据长度,如果超出64位则填充原数据长度的后64位,这样可以使得最终的数据长度为512的整数倍。

1.4处理消息块并更新上下文

void MD5::init(const byte* input, size_t len) {
bit32 i, index, partLen; finished
= false;
/* Compute number of bytes mod 64 */ index = (bit32)((count[0] >> 3) & 0x3f);//index 表示当前消息块中的字节数,并指示了 buffer 数组中的位置,以便将新的消息块拷贝到正确的位置。
/* update number of bits *///更新消息处理的比特数和字节数,并计算剩余空间 partLen if ((count[0] += ((bit32)len << 3)) < ((bit32)len << 3)) { ++count[1]; } count[1] += ((bit32)len >> 29); partLen = 64 - index;//缓冲区剩余位置的大小 /* transform as many times as possible. */ if (len >= partLen) {//检查输入消息的长度是否大于等于 partLen,如果大于的话: memcpy(&buffer[index], input, partLen);//将输入消息的一部分输入缓冲区 transform(buffer);//对缓冲区进行MD5转换 for (i = partLen; i + 63 < len; i += 64) {//从输入消息的剩余部分开始,每次取 64 个字节,进行 MD5 转换,直到剩余部分不足 64 个字节 transform(&input[i]); } index = 0;//缓冲区被填满了,重置为0 } else { i = 0; }//如果输入消息的长度小于 partLen,说明当前的消息不足以填满缓冲区,因此将 i 设置为 0,以确保下一次调用 init 时正确处理缓冲区。 /* Buffer remaining input */ memcpy(&buffer[index], &input[i], len - i);//将输入消息中剩余的部分(长度为 len - i)复制到缓冲区中的剩余空间。 }
  1. const byte* input:这是指向输入消息的指针,它是一个字节数组,表示要进行MD5哈希的数据。

  2. size_t len:这是输入消息的长度,以字节为单位。它告诉函数有多少字节的数据需要处理。

  1. count[0] += ((bit32)len << 3):首先,将 count[0] 更新为当前总比特数。((bit32)len << 3) 将字节数转换为比特数,并将其加到 count[0] 上。

  2. ((bit32)len >> 29):然后,将字节数右移 29 位,这样可以得到高 3 位的值。这是因为 count[1] 存储的是比特数的高 32 位。如果 count[0] 的更新导致溢出,那么需要将 count[1] 的值加一。

  3. partLen = 64 - index:计算剩余空间,即当前消息块 buffer 中还没有被填充的部分。这个值表示需要多少字节来填充当前消息块,使其达到 64 字节的长度。

  &input[i] 是输入消息的起始地址,len - i 是剩余消息的长度,&buffer[index] 是缓冲区中剩余空间的起始地址。这样做是为了保证在下一次调用 init 时,可以正确地处理缓冲区。

1.5基于block的状态转换

void MD5::transform(const byte block[64]) {

  bit32 a = state[0], b = state[1], c = state[2], d = state[3], x[16];

  decode(block, x, 64);

  block 是输入的64字节消息块,而 x 是一个包含16个32位字的数组,用于保存从 block 中解码得到的数据。

  a, b, c, d 是MD5算法中的四个32位寄存器,初始时它们包含了MD5算法的初始向量。

  decode(block, x, 64) 函数将输入的64字节消息块解码为16个32位字,存储在数组 x 中。

  /* Round 1 */
  FF (a, b, c, d, x[ 0], s11, 0xd76aa478);
  FF (d, a, b, c, x[ 1], s12, 0xe8c7b756);
  FF (c, d, a, b, x[ 2], s13, 0x242070db);
  FF (b, c, d, a, x[ 3], s14, 0xc1bdceee);
  FF (a, b, c, d, x[ 4], s11, 0xf57c0faf);
  FF (d, a, b, c, x[ 5], s12, 0x4787c62a);
  FF (c, d, a, b, x[ 6], s13, 0xa8304613);
  FF (b, c, d, a, x[ 7], s14, 0xfd469501);
  FF (a, b, c, d, x[ 8], s11, 0x698098d8);
  FF (d, a, b, c, x[ 9], s12, 0x8b44f7af);
  FF (c, d, a, b, x[10], s13, 0xffff5bb1);
  FF (b, c, d, a, x[11], s14, 0x895cd7be);
  FF (a, b, c, d, x[12], s11, 0x6b901122);
  FF (d, a, b, c, x[13], s12, 0xfd987193);
  FF (c, d, a, b, x[14], s13, 0xa679438e);
  FF (b, c, d, a, x[15], s14, 0x49b40821);

  /* Round 2 */
  GG (a, b, c, d, x[ 1], s21, 0xf61e2562);
  GG (d, a, b, c, x[ 6], s22, 0xc040b340);
  GG (c, d, a, b, x[11], s23, 0x265e5a51);
  GG (b, c, d, a, x[ 0], s24, 0xe9b6c7aa);
  GG (a, b, c, d, x[ 5], s21, 0xd62f105d);
  GG (d, a, b, c, x[10], s22,  0x2441453);
  GG (c, d, a, b, x[15], s23, 0xd8a1e681);
  GG (b, c, d, a, x[ 4], s24, 0xe7d3fbc8);
  GG (a, b, c, d, x[ 9], s21, 0x21e1cde6);
  GG (d, a, b, c, x[14], s22, 0xc33707d6);
  GG (c, d, a, b, x[ 3], s23, 0xf4d50d87);
  GG (b, c, d, a, x[ 8], s24, 0x455a14ed);
  GG (a, b, c, d, x[13], s21, 0xa9e3e905);
  GG (d, a, b, c, x[ 2], s22, 0xfcefa3f8);
  GG (c, d, a, b, x[ 7], s23, 0x676f02d9);
  GG (b, c, d, a, x[12], s24, 0x8d2a4c8a);

  /* Round 3 */
  HH (a, b, c, d, x[ 5], s31, 0xfffa3942);
  HH (d, a, b, c, x[ 8], s32, 0x8771f681);
  HH (c, d, a, b, x[11], s33, 0x6d9d6122);
  HH (b, c, d, a, x[14], s34, 0xfde5380c);
  HH (a, b, c, d, x[ 1], s31, 0xa4beea44);
  HH (d, a, b, c, x[ 4], s32, 0x4bdecfa9);
  HH (c, d, a, b, x[ 7], s33, 0xf6bb4b60);
  HH (b, c, d, a, x[10], s34, 0xbebfbc70);
  HH (a, b, c, d, x[13], s31, 0x289b7ec6);
  HH (d, a, b, c, x[ 0], s32, 0xeaa127fa);
  HH (c, d, a, b, x[ 3], s33, 0xd4ef3085);
  HH (b, c, d, a, x[ 6], s34,  0x4881d05);
  HH (a, b, c, d, x[ 9], s31, 0xd9d4d039);
  HH (d, a, b, c, x[12], s32, 0xe6db99e5);
  HH (c, d, a, b, x[15], s33, 0x1fa27cf8);
  HH (b, c, d, a, x[ 2], s34, 0xc4ac5665);

  /* Round 4 */
  II (a, b, c, d, x[ 0], s41, 0xf4292244);
  II (d, a, b, c, x[ 7], s42, 0x432aff97);
  II (c, d, a, b, x[14], s43, 0xab9423a7);
  II (b, c, d, a, x[ 5], s44, 0xfc93a039);
  II (a, b, c, d, x[12], s41, 0x655b59c3);
  II (d, a, b, c, x[ 3], s42, 0x8f0ccc92);
  II (c, d, a, b, x[10], s43, 0xffeff47d);
  II (b, c, d, a, x[ 1], s44, 0x85845dd1);
  II (a, b, c, d, x[ 8], s41, 0x6fa87e4f);
  II (d, a, b, c, x[15], s42, 0xfe2ce6e0);
  II (c, d, a, b, x[ 6], s43, 0xa3014314);
  II (b, c, d, a, x[13], s44, 0x4e0811a1);
  II (a, b, c, d, x[ 4], s41, 0xf7537e82);
  II (d, a, b, c, x[11], s42, 0xbd3af235);
  II (c, d, a, b, x[ 2], s43, 0x2ad7d2bb);
  II (b, c, d, a, x[ 9], s44, 0xeb86d391);

  state[0] += a;
  state[1] += b;
  state[2] += c;
  state[3] += d;
}

  一共进行四轮 Round 每个 Round 都包含16个操作。FF, GG, HH, II 函数分别对应 MD5 算法中的四个基本运算。s11, s12, ..., s44 是不同轮次中每一步的位移数。0xd76aa478, 0xe8c7b756, ..., 0xeb86d391 是不同轮次中的常数。

  这些运算共同作用于 MD5 寄存器状态 a, b, c, d,通过不同的运算方式,使得每轮循环都能影响寄存器状态,最终得到经过处理的 MD5 散列值。整个 transform 函数的目的是处理一个消息块,更新 MD5 寄存器状态,使其能够反映该消息块的特征。

1.7将输入编码输出为字节

 

void MD5::encode(const bit32* input, byte* output, size_t length) {

  for (size_t i = 0, j = 0; j < length; ++i, j += 4) {
    output[j]= (byte)(input[i] & 0xff);
    output[j + 1] = (byte)((input[i] >> 8) & 0xff);
    output[j + 2] = (byte)((input[i] >> 16) & 0xff);
    output[j + 3] = (byte)((input[i] >> 24) & 0xff);
  }
}

 

  • inputbit32 类型的输入数组,这里是寄存器状态数组。
  • outputbyte 类型的输出数组,用于存储编码后的字节序列。
  • length:编码的字节长度。

 

&input[i] 是输入消息的起始地址,len - i 是剩余消息的长度,&buffer[index] 是缓冲区中剩余空间的起始地址。这样做是为了保证在下一次调用 init 时,可以正确地处理缓冲区。
posted @ 2026-01-06 21:54  智慧人士橘猫  阅读(2)  评论(0)    收藏  举报