11#! /usr/bin/env lua
22
3+ --[[ Implicit global variables:
4+ SINE = MD5 sine constants table precomputed values based on sine function
5+ ]]
6+
37--- Get file hash
48--- @param file file The file to read
59--- @return string MD5 checksum
610function md5_file (file )
7- local MAX = 0xFFFFFFFF -- Maximum value of number (32-bits)
11+
12+ -- MAX = Maximum value of number (32-bits)
13+ local MAX = 0xFFFFFFFF
814
915 --- MD5 transformation function
1016 --- @param ch string A 64-byte chunk of the message
@@ -14,10 +20,11 @@ function md5_file(file)
1420 --- @param D number Fourth word of the current hash state
1521 --- @return number , number , number , number Updated hash state (A , B , C , D )
1622 local function T (ch , A , B , C , D )
17- -- F = Conditional function (if x then y else z)
18- -- G = Multiplexer function (if z then x else y)
19- -- H = Parity function (XOR of all inputs)
20- -- I = Nonlinear mixing function
23+
24+ -- F = Conditional function (if x then y else z)
25+ -- G = Multiplexer function (if z then x else y)
26+ -- H = Parity function (XOR of all inputs)
27+ -- I = Nonlinear mixing function
2128 -- LS = Left bit rotation
2229 local F , G , H , I , LS = function (x , y , z )
2330 return (x & y ) | (~x & z )
@@ -30,14 +37,30 @@ function md5_file(file)
3037 end , function (x , n )
3138 return ((x << n ) | (x >> (32 - n ))) & MAX
3239 end
40+
41+ -- wo = Word array for holding the 16 32-bit words from the current message chunk
42+ -- sh = shift amounts matrix
43+ -- a,b,c,d = Working copies of the hash state variables A,B,C,D
3344 local wo , sh , a , b , c , d = {}, { { 7 , 12 , 17 , 22 }, { 5 , 9 , 14 , 20 }, { 4 , 11 , 16 , 23 }, { 6 , 10 , 15 , 21 } }, A , B , C , D
34- for i = 0 , 15 do
45+
46+ for i = 0 , 15 do -- Convert into 16 32-bit little-endian words
47+
48+ -- o = Byte offset within the chunk
3549 local o = i * 4 + 1
3650 wo [i + 1 ] = string.byte (ch , o ) | (string.byte (ch , o + 1 ) << 8 ) | (string.byte (ch , o + 2 ) << 16 ) | (string.byte (ch , o + 3 ) << 24 )
3751 end
52+
3853 for i = 1 , 64 do
39- local r , f , g , t = math.floor ((i - 1 ) / 16 ) + 1
40- local s = sh [r ][(i - 1 ) % 4 + 1 ]
54+
55+ -- r = Round number (1 to 4)
56+ -- f = Result of round function
57+ -- g = Index into message word array
58+ -- t = Temporary variable for state transformation
59+ -- s = Shift amount for current step
60+ local r , f , g , t , s = math.floor ((i - 1 ) / 16 ) + 1
61+
62+ s = sh [r ][(i - 1 ) % 4 + 1 ]
63+
4164 if r == 1 then
4265 f = F (b , c , d )
4366 g = (i - 1 ) % 16
@@ -51,13 +74,16 @@ function md5_file(file)
5174 f = I (b , c , d )
5275 g = (7 * (i - 1 )) % 16
5376 end
77+
78+ -- Transform the state variables (a,b,c,d) according to MD5 algorithm
5479 t = d
5580 d = c
5681 c = b
5782 b = (b + LS ((a + f + wo [g + 1 ] + SINE [i ]) & MAX , s )) & MAX
5883 a = t
5984 end
6085
86+ -- Add the transformed chunk values to the hash state (modulo 2^32)
6187 A = (A + a ) & MAX
6288 B = (B + b ) & MAX
6389 C = (C + c ) & MAX
@@ -69,42 +95,68 @@ function md5_file(file)
6995 --- @param msgLen number The length of the message in bytes
7096 --- @return string The padding string to append to the message
7197 local function P (msgLen )
98+
99+ -- ml = Message length in bits
100+ -- p = padding string
101+ -- pl = padding length
72102 local ml , p , pl = msgLen * 8 , " \128 " , (56 - (msgLen % 64 ))
103+
73104 if pl <= 0 then
74105 pl = pl + 64
75106 end
107+
76108 p = p .. string.rep (" \0 " , pl - 1 )
109+
110+ -- Append the 64-bit message length as little-endian bytes
77111 for i = 0 , 7 do
78112 p = p .. string.char ((ml >> (8 * i )) & 0xFF )
79113 end
114+
80115 return p
81116 end
82117
118+ -- A,B,C,D = The four words that form the 128-bit MD5 state/digest
119+ -- l = total length of processed data
83120 local A , B , C , D , l = 0x67452301 , 0xefcdab89 , 0x98badcfe , 0x10325476 , 0
84- while 1 do
121+
122+ while 1 do -- Process the input file in 64-byte chunks
123+
124+ -- c = Chunk of data
85125 local c = file :read (64 )
86- if not c then
126+
127+ if not c then -- End of file
87128 break
88129 end
130+
89131 l = l + # c
90- if # c < 64 then
132+
133+ if # c < 64 then -- Pad the chunk if not 64 bytes long
91134 c = c .. P (l )
92135 end
136+
137+ -- Process this chunk through the MD5 transformation
93138 A , B , C , D = T (c , A , B , C , D )
139+
94140 if # c > 64 then
95141 break
96142 end
97143 end
144+
145+ --- Convert a 32-bit word to an 8-character hexadecimal string (little-endian)
146+ --- @param x number The 32-bit word to convert
147+ --- @return string The hexadecimal representation (8 characters )
98148 local function hex (x )
99149 return string.format (" %02x%02x%02x%02x" , x & 0xFF , (x >> 8 ) & 0xFF , (x >> 16 ) & 0xFF , (x >> 24 ) & 0xFF )
100150 end
151+
152+ -- Concatenate the four state words as hexadecimal to form the final 32-character MD5 digest
101153 return hex (A ) .. hex (B ) .. hex (C ) .. hex (D )
102154end
103155
104- --- Initialize the MD5 sine constants table
156+ --- Initialize the MD5 sine constants table with precomputed values used in each step of the MD5 algorithm
105157local function init ()
106158 SINE = {}
107- for i = 1 , 64 do
159+ for i = 1 , 64 do -- Formula from RFC 1321 truncated to 32 bits
108160 SINE [i ] = math.floor (2 ^ 32 * math.abs (math.sin (i )))
109161 end
110162end
@@ -120,9 +172,16 @@ if #arg < 1 then
120172else
121173 init ()
122174 for i = 1 , # arg do
175+
176+ -- f = open file handle
177+ -- e = error string if file failed to open
123178 local f , e = io.open (arg [i ], " rb" ) -- #squish keep-eol
179+
124180 if f then
181+
182+ -- sum = MD5 string of file
125183 local sum = md5_file (f )
184+
126185 f :close ()
127186 if sum then
128187 print (sum .. " " .. arg [i ])
0 commit comments