一、实验环境
本实验是在python3.9环境下运行的,在spyder上完成运行。
二、实验内容
MD5算法的具体实现。
三、总体思路
copy的图:
总体来说,首先对于原消息,要将其填充,最终填充后的长度可以被512bit整除。填充后,将每512bit作为一块(之后说到一块,就指每一次的512bit),将每一块以及一个128bit的初始序列作为输入,进行MD5分组处理的运算。在每一块的分组运算中,有4轮(之后说到轮,就是每一块的4轮),64步(之后说到步,就是指一块中的64步),也就是一轮有16步。每一块运算完成后,会有一个128bit的输出,这个输出会作为下一块的运算的一个输入,再加上下一块的512bit,作为下一块MD5运算的输入量,之后不断循环,直到所有bit都运算过。最后,将最后一次运算的128bit的输出作为结果,即MD5加密的值。
四、实验步骤
1.设置基本参数
在MD5运算中有许多参数需要设置,包括初始的4个32bit的缓冲区A、B、C、D,以及他们的复制AA、BB、CC、DD;表T;表S(压缩函数每步左循环移位的位数);表m(每轮对使用字的顺序的处理);F、G、H、I基本逻辑函数;F_、G_、H_、I_函数(每一轮中每一步函数的实现);CLS函数(左循环移位函数)。
1.1 A、B、C、D缓冲区以及他们的复制AA、BB、CC、DD。
设置缓冲区的目的是存储中间结果以及最终的MD5值,每个寄存器都以小端方式存储数据。初始值为A = 0x01234567,B = 0x89ABCDEF,C = 0xFEDCBA98,D = 0x76543210,按照小端方式存储后,结果为A = 0X67452301,B = 0XEFCDAB89,C = 0X98BADCFE,D = 0X10325476。同时设置他们的复制,方便在每一轮循环最后,将改变后的ABCD与最初输入的值进行相加。
A = 0X67452301
B = 0XEFCDAB89
C = 0X98BADCFE
D = 0X10325476
AA = 0X67452301
BB = 0XEFCDAB89
CC = 0X98BADCFE
DD = 0X10325476
1.2 表T
书上提供了常数表T。T[i]在每一轮中的每一步中,需要T[i]的参与。一块一共64步,恰好遍历64个T[i]。
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 ,]
1.3 表S
书上提供了表S。表S中的S[i]是CLS函数中需要用到的,表示每步左循环移位的位数。与T[i]类似,恰好64步,遍历64个S[i]。
S = [ 7 , 12 , 17 , 22 , 7 , 12 , 17 , 22 ,
7 , 12 , 17 , 22 , 7 , 12 , 17 , 22 ,
5 , 9 , 14 , 20 , 5 , 9 , 14 , 20 ,
5 , 9 , 14 , 20 , 5 , 9 , 14 , 20 ,
4 , 11 , 16 , 23 , 4 , 11 , 16 , 23 ,
4 , 11 , 16 , 23 , 4 , 11 , 16 , 23 ,
6 , 10 , 15 , 21 , 6 , 10 , 15 , 21 ,
6 , 10 , 15 , 21 , 6 , 10 , 15 , 21 ,]
1.4 表m
表m实际上是对代码的一步简化。因为在函数的一块处理中,输入的512bit要分成16个32bit的字,在64步中,每16步要循环利用一遍这16个字。第一遍为0-15,恰好也利用的是F_函数。在第一遍中,16步的每一步都需要加X[k],而这里的X[k]即为M[k],即从M[0]到M[15]的字。而在第二遍中,即G_函数中,需要用到字依然是M[0]-M[15],但是用字的顺序发生了改变,书上提供了算法,置换算法如下:
而表m即将这每16步的字的顺序算好,存到了m中,为之后调用提供便利。
m = [ 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 ]
1.5 F、G、H、I基本逻辑函数
这四个逻辑函数是在每一步中都需要使用到其中一种,每个函数都是简单的逻辑运算函数,书上给出了公式。
def F(a, b, c):
return (a & b) | ((~a) & c)
def G(a, b, c):
return (a & c) | (b & (~c))
def H(a, b, c):
return a ^ b ^ c
def I(a, b, c):
return b ^ (a | (~c))
1.6 F_、G_、H_、I_函数
这四个函数表示的是每一步中整体的运算函数。书上给出了完整的过程。首先是B、C、D进行g函数(即FGHI)运算,之后与A相加,与X[k]相加,与T[i]相加。接下来进行CLS左循环移位,再与B在进行相加,这样就得到了B的结果。最后将原来B、C、D的结果分别传给C、D、A,这一步就计算完成了。贴了一个函数,剩下几个基本一致。
这里需要注意的是,由于我申请的A、B、C、D都是全局变量,在python中需要使用global声明才能对全局变量进行修改。另外,在将x与其他数相加的时候,要使用int转化,且必须有后面的2,只有这样才是按照2进制来解释x,否则会自动默认按照十进制解释x,导致结果错误。
def F_(x, t, s):
global A, B, C, D
g = F(B, C, D)
tem = g + A + int(x,2) + t
te = CLS(tem, s)
a1 = (te + B) % pow(2, 32)
a2, a3, a4 = B, C, D
A, B, C, D = a4, a1, a2, a3
1.7 CLS函数
CLS函数是32位存数左循环s位函数,s的取值在表S中可以找到。表示m向左移动n位。
def CLS(m, n):
m &= 0xFFFFFFFF
return (( m << n ) | (m >> (32 - n))) & 0xFFFFFFFF
m &= 0xFFFFFFFF这一步保证m是一个32位无符号整数。之后一个向左移位,一个向右移位是因为循环,向左到头就会到右移位。
到这里参数基本就已经引用完成了,接下来就正式进入MD5加密!
2.打开文件
由于是图片,所以用open打开文件,之后用read读取文件。需要注意的是,read读取文件读到的是'bytes'类型,需要转化成二进制,也就是逐比特来运算。
with open('jsf.jpg', 'rb') as f:
Data = f.read()
data = ''.join(format(byte, '08b') for byte in Data)
在转化过程中,‘08b’分别表示了使用零填充、总宽度为 8 个bit和二进制格式进行转换。
3.消息填充
消息填充分两步,第一步是填充1和0,第二步是填充消息长度。
3.1 填充1和0
填充这一步书上讲的还是比较清楚的,假设原长度为L1,填充后长度为L2,则满足
且
。填充的第一位为1,之后都为0。
3.2填充消息长度
在填充消息长度这一步中,由于上述长度与512bit差了64bit,我们要填充的消息长度就是64bit,正好填完,填充的内容是:在填充1和0之前的原消息的长度,这里用的是bit数。最后将填充的内容与原来的消息连接在一起。注意填充方式要使用小端填充
length_padding = struct.pack('<Q', length)
length_padding_bits = ''.join(format(byte, '08b') for byte in length_padding)
data += length_padding_bits
现在填充完成,数据应该正好是512bit的倍数。
4.分组处理
我们上面已经提到,MD5在处理过程中,是以512bit为一块进行处理,下面首先针对一块的处理进行分析。
4.1 对一块消息的处理
4.1.1 总体处理
针对每一块消息,对照下图。我们的输入有两个,CVq和Yq。CVq为128bit的串,其实就是定义的A、B、C、D四个32bit的缓冲区,连在一起就成了128bit的CVq。Yq为512bit的块,将其分成16个32bit的字。这里特别强调一下,我认为“字”是MD5这个算法的关键!
4.1.2 求“字”
在这一块中,我们需要持续用到这16个字,而且这16个字是独一无二属于这512bit的,在下一个块中,会用到另外16个字。那么首先就先将这个16个字给搞出来。
下面是求一块中16个字的方法:
for i in range(0, len(data), 512):
M = []
cur_group = data[i : i + 512]
for j in range(0, 512, 32):
num = ""
# 获取每一段的标准十六进制形式
for k in range(0, len(cur_group[j:j+32]), 4):
temp = cur_group[j:j+32][k:k+4]
temp = hex(int(temp, 2))
num += temp[2]
# 对十六进制进行小端排序
num_tmp = ""
for k in range(8, 0, -2):
temp = num[k-2:k]
num_tmp += temp
mm = ""
for k in range(len(num_tmp)):
num = int(num_tmp[k], 16)
bin_num = bin(num)[2:].zfill(4)
mm += bin_num
M.append(mm)
首先是创建列表M,用来存储这一块的16个字。我们一次看32个bit,首先找到当前这32bit,4位一组求其十六进制数,用hex转化后将前两位“0x”舍掉,从temp[2]开始加到num中。之后对16进制数进行小端排序,倒序来看,每次一个字节(8bit),也就是2位16进制。最后将排序完成的数转化成二进制读出来,添加到M中。
这里学到一个小知识,int(x, k)表示将x从k进制转化为十进制,如果不写k,就默认是10。这里是将16进制的数解释为10进制整数,之后再转化为2进制即可。
4.1.3 64步迭代
由于在上面的F_、G_、H_、I_函数已经实现(详见1.6),这里可以直接使用。那我们就直接对函数进行使用,64步共分为4轮,第一轮全部使用的是F_函数,第二轮全部使用的是G_函数,第三轮全部使用的是H_函数,第四轮全部使用的是I_函数。一轮共有16步,总共64步。
这里需要注意的点是,我们在引用参数的时候,T和S较为简单,就是第几步就是几,但是对于M,由于一共只有16个字(上面刚求过),一次只够一轮(16个)使用。所以书上为我们提供了置换算法,第一次是从M[0]到M[15]顺序使用,之后每一次都使用的顺序不一样,而我们已经将其存到表m中了,所以直接调用表m即可(详见1.4)。
在函数64步迭代完成后,要将变换后的A、B、C、D与原来的A、B、C、D相加,这里我们用到之前设置的A、B、C、D的复制AA、BB、CC、DD即可。注意这里是模2的32次方相加。之后要将AA、BB、CC、DD重新设置为A、B、C、D,这是为了方便下一轮进行运算。
for ii in range(0, 16):
F_(M[m[ii]], T[ii], S[ii])
for ii in range(0, 16):
G_(M[m[ii + 16]], T[16 + ii], S[16 + ii])
for ii in range(0, 16):
H_(M[m[ii + 32]], T[32 + ii], S[32 + ii])
for ii in range(0, 16):
I_(M[m[ii + 48]], T[48 + ii], S[48 + ii])
A = (A + AA) % pow(2, 32)
B = (B + BB) % pow(2, 32)
C = (C + CC) % pow(2, 32)
D = (D + DD) % pow(2, 32)
AA, BB, CC, DD = A, B, C, D
通过上述对每一块消息的处理,不难发现,消息处理的实质就是对A、B、C、D四个缓冲区里的数据进行修改,调换,最后相加后,置换成新的A、B、C、D和AA、BB、CC、DD。A、B、C、D这四个32bit的缓冲区实质就是128bit的CVq。在中间处理过程中,没必要将其合并,只需要保持A、B、C、D即可。
4.2 对所有消息的处理
其实就是加上一个循环即可,很简单。512bit一块进行运算,不断更新A、B、C、D。
5.输出MD5值
当进行完最后一轮A、B、C、D的更新运算后,在循环外,将A、B、C、D分别以小端方式连接在一起,之后转化成16进制形式的字符串,即为最终的MD5码。
A_bytes = struct.pack('<I', A)
B_bytes = struct.pack('<I', B)
C_bytes = struct.pack('<I', C)
D_bytes = struct.pack('<I', D)
# 级联这些字节串
md5_digest = A_bytes + B_bytes + C_bytes + D_bytes
# 将级联后的字节串转换为16进制格式的字符串
md5_hex = ''.join(f'{byte:02x}' for byte in md5_digest)
print("MD5加密值:",md5_hex)
五、实验代码
下面是我写的完整代码,其中路径地址可以修改。如果想加密字符串,将Data数据改成b"string"形式即可。
import struct
A = 0X67452301
B = 0XEFCDAB89
C = 0X98BADCFE
D = 0X10325476
AA = 0X67452301
BB = 0XEFCDAB89
CC = 0X98BADCFE
DD = 0X10325476
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 ,]
S = [ 7 , 12 , 17 , 22 , 7 , 12 , 17 , 22 ,
7 , 12 , 17 , 22 , 7 , 12 , 17 , 22 ,
5 , 9 , 14 , 20 , 5 , 9 , 14 , 20 ,
5 , 9 , 14 , 20 , 5 , 9 , 14 , 20 ,
4 , 11 , 16 , 23 , 4 , 11 , 16 , 23 ,
4 , 11 , 16 , 23 , 4 , 11 , 16 , 23 ,
6 , 10 , 15 , 21 , 6 , 10 , 15 , 21 ,
6 , 10 , 15 , 21 , 6 , 10 , 15 , 21 ,]
m = [ 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 ]
def F(a, b, c):
return (a & b) | ((~a) & c)
def G(a, b, c):
return (a & c) | (b & (~c))
def H(a, b, c):
return a ^ b ^ c
def I(a, b, c):
return b ^ (a | (~c))
def CLS(m, n):
m &= 0xFFFFFFFF
return (( m << n ) | (m >> (32 - n))) & 0xFFFFFFFF
def F_(x, t, s):
global A, B, C, D
g = F(B, C, D)
tem = g + A + int(x,2) + t
te = CLS(tem, s)
a1 = (te + B) % pow(2, 32)
a2, a3, a4 = B, C, D
A, B, C, D = a4, a1, a2, a3
def G_(x, t, s):
global A, B, C, D
g = G(B, C, D)
tem = g + A + int(x,2) + t
te = CLS(tem, s)
a1 = (te + B) % pow(2, 32)
a2, a3, a4 = B, C, D
A, B, C, D = a4, a1, a2, a3
def H_(x, t, s):
global A, B, C, D
g = H(B, C, D)
tem = g + A + int(x,2) + t
te = CLS(tem, s)
a1 = (te + B) % pow(2, 32)
a2, a3, a4 = B, C, D
A, B, C, D = a4, a1, a2, a3
def I_(x, t, s):
global A, B, C, D
g = I(B, C, D)
tem = g + A + int(x,2) + t
te = CLS(tem, s)
a1 = (te + B) % pow(2, 32)
a2, a3, a4 = B, C, D
A, B, C, D = a4, a1, a2, a3
# 打开文件
with open('jsf.jpg', 'rb') as f:
Data = f.read()
#Data = b"hello world"
data = ''.join(format(byte, '08b') for byte in Data)
# 填充数据
length = len(data)
if length % 512 == 448:
data += '1'
data += '0' * 511
else:
t = (length + 64) % 512
te = 512 - t
if te != 1:
data += '1'
data += '0' * (te - 1)
else:
data += '0'
length_padding = struct.pack('<Q', length)
length_padding_bits = ''.join(format(byte, '08b') for byte in length_padding)
data += length_padding_bits
#循环加密
#求出M
for i in range(0, len(data), 512):
M = []
cur_group = data[i : i + 512]
for j in range(0, 512, 32):
num = ""
# 获取每一段的标准十六进制形式
for k in range(0, len(cur_group[j:j+32]), 4):
temp = cur_group[j:j+32][k:k+4]
temp = hex(int(temp, 2))
num += temp[2]
# 对十六进制进行小端排序
num_tmp = ""
for k in range(8, 0, -2):
temp = num[k-2:k]
num_tmp += temp
mm = ""
for k in range(len(num_tmp)):
num = int(num_tmp[k], 16)
bin_num = bin(num)[2:].zfill(4)
mm += bin_num
M.append(mm)
for ii in range(0, 16):
F_(M[m[ii]], T[ii], S[ii])
for ii in range(0, 16):
G_(M[m[ii + 16]], T[16 + ii], S[16 + ii])
for ii in range(0, 16):
H_(M[m[ii + 32]], T[32 + ii], S[32 + ii])
for ii in range(0, 16):
I_(M[m[ii + 48]], T[48 + ii], S[48 + ii])
A = (A + AA) % pow(2, 32)
B = (B + BB) % pow(2, 32)
C = (C + CC) % pow(2, 32)
D = (D + DD) % pow(2, 32)
AA, BB, CC, DD = A, B, C, D
A_bytes = struct.pack('<I', A)
B_bytes = struct.pack('<I', B)
C_bytes = struct.pack('<I', C)
D_bytes = struct.pack('<I', D)
# 级联这些字节串
md5_digest = A_bytes + B_bytes + C_bytes + D_bytes
# 将级联后的字节串转换为16进制格式的字符串
md5_hex = ''.join(f'{byte:02x}' for byte in md5_digest)
print("MD5加密值:",md5_hex)
六、实验总结
这个实验我是参考了网上很多代码,本身这个算法也比较简单,只要理解透彻了其实不算很难。当然说着不难还是有点难的,我感觉令我收获最大的就是修改代码的过程。因为我借鉴的那篇博客(上文提到)他只能运行通过小于512bit的内容,我就一点一点对着修改。不是对着代码改,是对着输出的结果改。就是参照它的输出结果和我的输出结果,中间步骤的运行结果,比如填充的结果、一步加密的结果、一轮加密中间量的结果等等,一点一点对着修改,最后成功写出来。
第一次写博客,之后还会多多写,确实写一遍之后收获蛮大!
哦哦,最后还有一点,就是我是怎么验证的。在linux环境下,输入
md5sum jsf.jpg
即可得到某文件的MD5加密结果,这个结果与代码运行结果是一致的。