Matt
Matt

Reputation: 136

Modified PHP MD5 gives different hashes

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

Upvotes: 1

Views: 691

Answers (1)

DaveRandom
DaveRandom

Reputation: 88647

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.

Upvotes: 2

Related Questions