Skip to content

Instantly share code, notes, and snippets.

@natmchugh
Last active August 10, 2018 15:08
Show Gist options
  • Save natmchugh/9780207 to your computer and use it in GitHub Desktop.
Save natmchugh/9780207 to your computer and use it in GitHub Desktop.
Pure PHP implementation of SHA1from wikipedia pseudo code
<?php
/*
Note 1: All variables are unsigned 32 bits and wrap modulo 232 when calculating, except
ml the message length which is 64 bits, and
hh the message digest which is 160 bits.
Note 2: All constants in this pseudo code are in big endian.
Within each word, the most significant byte is stored in the leftmost byte position
*/
// Initialize variables:
function preProcess($message){
/*
Pre-processing:
append the bit '1' to the message i.e. by adding 0x80 if characters are 8 bits.
append 0 ≤ k < 512 bits '0', thus the resulting message length (in bits)
is congruent to 448 (mod 512)
append ml, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
*/
$originalSize = strlen($message) * 8;
$message .= chr(128);
while (((strlen($message) + 8) % 64) !== 0) {
$message .= chr(0);
}
foreach (str_split(sprintf('%064b', $originalSize), 8) as $bin) {
$message .= chr(bindec($bin));
}
return $message;
}
function rotl($x, $n) {
return ($x << $n) | ($x >> (32 - $n));
}
function SHAfunction($step, $b, $c, $d)
{
switch ($step) {
case 0;
return ($b & $c) ^ (~$b & $d);
case 1;
case 3;
return $b ^ $c ^ $d;
case 2;
return ($b & $c) ^ ($b & $d) ^ ($c & $d);
}
}
function hash_sha1($input) {
$h0 = 0x67452301;
$h1 = 0xEFCDAB89;
$h2 = 0x98BADCFE;
$h3 = 0x10325476;
$h4 = 0xC3D2E1F0;
$K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
$message = preProcess($input);
// Process the message in successive 512-bit chunks:
// break message into 512-bit chunks
$chunks = str_split($message, 64);
foreach ($chunks as $chunk) {
// break chunk into sixteen 32-bit big-endian words w[i], 0 ≤ i ≤ 15
$words = str_split($chunk, 4);
foreach ($words as $i => $chrs) {
$chrs = str_split($chrs);
$word = '';
foreach ($chrs as $chr) {
$word .= sprintf('%08b', ord($chr));
}
$words[$i] = bindec($word);
}
// Extend the sixteen 32-bit words into eighty 32-bit words:
for ($i = 16; $i < 80; $i++) {
// for i from 16 to 79
// w[i] = (w[i-3] xor w[i-8] xor w[i-14] xor w[i-16]) leftrotate 1
$words[$i] = rotl($words[$i-3] ^ $words[$i-8] ^ $words[$i-14] ^ $words[$i-16], 1) & 0xffffffff;
}
// Initialize hash value for this chunk:
$a = $h0; $b = $h1; $c = $h2; $d = $h3; $e = $h4;
// Main loop:[39]
foreach ($words as $i => $word) {
$s = floor($i / 20);
$f = SHAfunction($s, $b, $c, $d);
$temp = rotl($a, 5) + $f + $e + $K[$s] + ($word) & 0xffffffff;
$e = $d;
$d = $c;
$c = rotl($b, 30);
$b = $a;
$a = $temp;
}
// Add this chunk's hash to result so far:
$h0 = ($h0 + $a) & 0xffffffff;
$h1 = ($h1 + $b) & 0xffffffff;
$h2 = ($h2 + $c) & 0xffffffff;
$h3 = ($h3 + $d) & 0xffffffff;
$h4 = ($h4 + $e) & 0xffffffff;
}
return sprintf('%08x%08x%08x%08x%08x', $h0, $h1, $h2, $h3, $h4);;
}
echo sha1('password'), PHP_EOL;
echo hash_sha1('password'), PHP_EOL;
echo hash_sha1(file_get_contents(__FILE__)), PHP_EOL;
echo sha1_file(__FILE__), PHP_EOL;
@spello
Copy link

spello commented Oct 25, 2016

sha1('password') is not equal to hash_sha1('password') so sadly something is wrong in your code.

@Demonslay335
Copy link

Demonslay335 commented Aug 10, 2018

The code is actually correct. SHA1 works with unsigned 32-bit integers[1], but PHP only supports signed 32-bit integers, so all of the calculations within the main loop turn out wrong because they are wrapping around to be a negative signed integer. You need to use 64-bit (signed) integers to overcome this.

May be good to note this code only works on 64-bit PHP.

<?php
echo PHP_INT_MAX . PHP_EOL;
echo "sha1('password') = " . sha1('password') . PHP_EOL;
echo "hash_sha1('password') = " . hash_sha1('password') . PHP_EOL;

/*
32-bit PHP Output:
PHP_INT_MAX = 2147483647
sha1('password') = 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
hash_sha1('password') = 322f89ebdf649e7f850e07c1fd0bdf0d134ce6b9

64-bit PHP Output:
PHP_INT_MAX = 9223372036854775807
sha1('password') = 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
hash_sha1('password') = 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
*/
?>

Theoretically, you could get this to work on 32-bit PHP using the something like the BC Math or GMP extensions and some re-writing of the functions above.

[1]: except the message length (ml) is treated as a 64-bit unsigned integer in big endian, but you are more likely to run out of RAM or PHP memory limits first due to trying to hash a string larger than 2^32 bytes in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment