1

I have a modified MD5 hash function which I am using in PHP and VB.NET. When I run the PHP code on my local server (WAMP) I get a different result to the VB version. I have tried running the script on phpfiddle which gives the same result as the VB version.

I am thinking the problem could lie with my PHP settings on the WAMP server?

If I run the script below on my PC running WAMP the result I get is:

e5c35f7c3dea80fc68a4031582f34c25

When I run the exact same script on phpfiddle or php sandbox the result I get is (this is the expected result):

6337a43e8cd36058e80ae8cb4f465998

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Matt
  • 136
  • 2
  • 10
  • 2
    In what way is it modified? What input are you trying? What output do you expect? – Ry- Jun 30 '13 at 20:10
  • 2 lines were changed. It's not the modifications that are causing the problem as the script runs fine as mentioned on websites like phpfiddle. – Matt Jun 30 '13 at 20:13
  • 2
    That didn’t answer any of the questions… – Ry- Jun 30 '13 at 20:14
  • HH ($C, $D, $A, $B, $words[11 + ($i * 16)], 16, "6d9d6122"); the 16 was changed to 14 also one line was added twice $this->II ($B, $C, $D, $A, $words[9 + ($i * 16)], 21, "eb86d391"); – Matt Jun 30 '13 at 20:17
  • Okay. So what input are you trying and what output do you expect? Is it plain text? Does it have higher characters than ASCII can manage? This sort of thing is usually an encoding problem. – Ry- Jun 30 '13 at 20:19
  • The input I tried (shown in the code) is "test". I've added the expected output to the question. Apologies for not making it clearer. – Matt Jun 30 '13 at 20:21
  • Oh, `test` was the one breaking it? That’s unusual! :) When I run it, I get `6337a43e8cd36058e80ae8cb4f465998` too… is your local PHP up-to-date? – Ry- Jun 30 '13 at 20:22
  • @Matt maybe you have problems in PHP with overflowing ints (32 bit?) which are converted to float? (and in VB: normal overflow)? – bwoebi Jun 30 '13 at 20:23
  • @bwoebi: VB.NET usually has checked overflows. – Ry- Jun 30 '13 at 20:24
  • @minitech since PHP 5 it works according to http://3v4l.org/S5BL1 – bwoebi Jun 30 '13 at 20:24
  • @minitech I am running 5.4.12 – Matt Jun 30 '13 at 20:25
  • @bwoebi I think you may be onto something, I am running the 64-bit version of WAMP, that could be it? – Matt Jun 30 '13 at 20:26
  • @minitech maybe the byte length of the integer is different in VB.NET (32 bit int or 64 bit int?) – bwoebi Jun 30 '13 at 20:28
  • 1
    @Matt `print PHP_INT_MAX;` gives `9223372036854775807` for you? – bwoebi Jun 30 '13 at 20:28
  • @bwoebi No I get 2147483647! – Matt Jun 30 '13 at 20:30
  • @Matt then you're using the 32-bit version of PHP (or your os is simply 32 bit). You may want to try on another server/upgrade your OS to 64 bit? – bwoebi Jun 30 '13 at 20:31
  • @bwoebi Is there any way to get it to work on the 32bit version? – Matt Jun 30 '13 at 20:32
  • @Matt maybe, didn't dig into the code exactly. – bwoebi Jun 30 '13 at 20:33
  • maybe your problem in file encoding Encode in UTF-8 – Ahmed Atta Jun 30 '13 at 20:42
  • 1
    @Matt If you change all instances of `0xffffffff` to `-1` it *may* fix it for this specific case, but not in the general case. The problem is that as soon as a number exceeds `PHP_INT_MAX` it becomes a float, and doesn't play nice with bitwise operations. What you need to do to fix it is write "binary safe" addition function (and change all `+`s to calls to that function), to ensure that all additions result in the correct binary representation of the result. The root of the problem is that PHP doesn't have any concept of unsigned integers – DaveRandom Jun 30 '13 at 20:49
  • @DaveRandom I tried your suggestion but still got the same issue. I will look into the binary safe function, do you have any more advice? Thanks for your suggestions – Matt Jun 30 '13 at 21:14
  • 5
    **WHY** the heck are you creating a custom `MD5` function? This smells of *you're doing it horribly wrong*. I understand you may find yourself in a situation where this looks like the correct approach, but seriously, it's not. Ever. Fix the original function to use a more appropriate hashing algorithm (like SHA-2 or SHA-3), and migrate the existing hashes. But **don't** perpetuate the horrors here... – ircmaxell Jul 01 '13 at 01:29

1 Answers1

2

Setting aside for a moment the fact that what you are doing here sounds like a bad approach what ever the actual problem is that you are trying to solve, here is a direct answer to the question.


As I already outlined in a comment above, the root cause of the problems you are having is that PHP has no concept of unsigned integers, and it handles this by converting numbers that overflow the bounds of an integer to floating point (which doesn't play nice with bitwise operations). This means that, on 32-bit systems, your code won't work correctly, as MD5 works with unsigned 32-bit integers.

You will need to ensure that your code is "binary safe" - so that all numbers are represented as if they were unsigned 32-bit integers.

To do this you will need to re-implement the addition operator, and (with your current implementation) the bindec()/hexdec() functions. It's worth noting that your current approach to certain procedures is very inefficient - all that converting to/from hex strings, and places where binary is represented as ASCII strings - but I'll gloss over that for now while I show you how to quick-fix your current implementation.

Firstly let's take a look at the addition operation:

private function binarySafeAddition($a, $b)
{
    // NB: we don't actually need 64 bits, theoretically we only need 33
    // but 40 bit integers are confusing enough, and 33 bits is unrepresentable
    $a = "\x00\x00\x00\x00" . pack('N', $a);
    $b = "\x00\x00\x00\x00" . pack('N', $b);

    $carry = $a & $b;
    $result = $a ^ $b;

    while ($carry != "\x00\x00\x00\x00\x00\x00\x00\x00") {
        $shiftedcarry = $this->leftShiftByOne($carry);
        $carry = $result & $shiftedcarry;
        $result ^= $shiftedcarry;
    }

    return current(unpack('N', substr($result, 4)));
}

private function leftShiftByOne($intAsStr)
{
    $p = unpack('N2', $intAsStr);
    return pack('N2', ($p[1] << 1) | (($p[2] >> 31) & 0x00000001), $p[2] << 1);
}

private function add()
{
    $result = 0;

    foreach (func_get_args() as $i => $int) {
        $result = $this->binarySafeAddition($result, $int);
    }

    return $result;
}

The real nuts-and-bolts of this routine is shamelessly stolen from here. There's also a helper function to perform the left-shift, because PHP doesn't let you left-shift strings, and a convenience wrapper function, to allow us to add an arbitrary number of operands together in a single clean call.

Next lets look at the bindec() and hexdec() replacements:

private function binarySafeBinDec($bin)
{
    $bits = array_reverse(str_split($bin, 1));
    $result = 0;

    foreach ($bits as $position => $bit) {
        $result |= ((int) $bit) << $position;
    }

    return $result;
}

private function binarySafeHexDec($hex)
{
    $h = str_split(substr(str_pad($hex, 8, '0', STR_PAD_LEFT), -8), 2);
    return (hexdec($h[0]) << 24) | (hexdec($h[1]) << 16) | (hexdec($h[2]) << 8) | hexdec($h[3]);
}

Hopefully these are reasonably self explanatory, but feel free to ask about anything you don't understand.

We also need to replace all those 0xffffffff hex literals with a binary safe implementation, as these will also result in a float on 32-bit systems. Here is a safe way to get the right-most 32 bits set in an integer, that will work on 32- and 64-bit systems:

private $right32;

public function __construct()
{
    $this->right32 = ~((~0 << 16) << 16);
}

There's one other method we need to re-implement, and that's rotate(). This is because it uses a right-shift, and this shifts a copy of the sign bit on from the right. This means that the left-hand side of the rotated block will end up with all it's bits set, and this is obviously not what we want. We can overcome this by creating a number with only the target bits for the right-hand side set, and ANDing the right-hand side operand with it:

private function rotate ($decimal, $bits)
{
    return dechex(($decimal << $bits) | (($decimal >> (32 - $bits)) & (~(~0 << $bits) & $this->right32)));
}

When you put all this together you come up with something like this, which works for me on 32- and 64-bit systems.

Community
  • 1
  • 1
DaveRandom
  • 87,921
  • 11
  • 154
  • 174