Jon-G blogs for Net-Entwicklung.de

17.06.2012

Hashing passwords and other data in PHP… the ultimate hashing class? ;)

Filed under: Coding — Jonathan Gilbert @ 01:00

Here is a pretty robust and easy to use class to encrpty passwords. Actually we should call it hashing, not encryption because encryption implies that decryption is possible. With one-way hashes decryption is not possible. To use it, just create a subclass of the abstract class (example below) where you optionally set a hashing algorithm. If you don’t set it, the most secure algorithm gets chosen automatically. Instantiate the class and call createHash to create a hash:

    $hashObj = new MyHash();
    $hash = $hashObj->createHash($data);

and to verify a hash, use verifyHash:

    $hashObj = new MyHash();
    $verified = $hashObj->verifyHash($data, $hash);

You can verify hashes created with different algorithms and, because the data is serialized, you can hash and verify any variable type, not just strings as most hashing classes do.

Any thoughts? Improvements? Disadvantages? Errors?

/**
 * Hashing
 * @author jon.gilbert@net-entwicklung.de
 */
abstract class AbstractHash
{
    const HASH_METHOD_MD5    = 'md5';
    const HASH_METHOD_SHA256 = 'sha256';
    const HASH_METHOD_SHA512 = 'sha512';
    const HASH_METHOD_BLOWFISH = 'blowfish';

    const SALT_LENGTH_MD5      = 12;
    const SALT_LENGTH_SHA256   = 16;
    const SALT_LENGTH_SHA512   = 16;
    const SALT_LENGTH_BLOWFISH = 22;

    protected $minIterationsSha256 = 1000;
    protected $maxIterationsSha256 = 50000;  // absolute maximum 999999999

    protected $minIterationsSha512 = 1000;
    protected $maxIterationsSha512 = 50000;  // absolute maximum 999999999

    protected $minIterationsBlowfish = 4;
    protected $maxIterationsBlowfish = 14;  // absolute maximum 31

    protected $minIterationsMd5 = 1;
    protected $maxIterationsMd5 = 1;  // absolute maximum 31

    /**
     * @var string - hash method to be used (one of HASH_METHOD_ constants)
     */
    private $hashMethod = null;

    /**
     * @var int - number of iterations
     */
    private $iterations = null;

    private $minIterations = null;
    private $maxIterations = null;
    private $saltLength    = null;

    /**
     * verify that a hash is correct
     *
     * @param mixed  $data
     * @param string $hash
     * @throws \Exception if hash not correct
     * @return true
     */
    public function verifyHash($data, $hash)
    {
        $salt = $this->getSaltFromHashed($hash);
        return $this->verifyHashWithSalt($data, $salt, $hash);
    }

    /**
     * extract salt from a given hashed string
     * @param string $hashed
     * @return string
     */
    protected function getSaltFromHashed($hashed)
    {
        // test for blowfisch
        $pattern = '/^(\$2a\$[0-3][0-9]\$.{' . self::SALT_LENGTH_BLOWFISH . '}).*$/';
        preg_match($pattern, $hashed, $matches);
        if (isset($matches[1]))
        {
            $this->setHashMethod(self::HASH_METHOD_BLOWFISH);
            $salt = $matches[1];
            return $salt;
        }

        // test for SHA512
        $pattern = '/^(\$6\$rounds=[0-9]+\$)(.{' . self::SALT_LENGTH_SHA512 . '}\$).*$/';
        preg_match($pattern, $hashed, $matches);
        if (isset($matches[2]))
        {
            $salt = $matches[1] . $matches[2];
            $this->setHashMethod(self::HASH_METHOD_SHA512);
            return $salt;
        }

        // test for SHA256
        $pattern = '/^(\$5\$rounds=[0-9]+\$)(.{' . self::SALT_LENGTH_SHA256 . '}\$).*$/';
        preg_match($pattern, $hashed, $matches);
        if (isset($matches[2]))
        {
            $salt = $matches[1] . $matches[2];
            $this->setHashMethod(self::HASH_METHOD_SHA256);
            return $salt;
        }

        // test for MD5
        $pattern = '/^(\$1\$)(.{' . self::SALT_LENGTH_MD5 . '}).+$/';
        preg_match($pattern, $hashed, $matches);
        if (isset($matches[2]))
        {
            $this->setHashMethod(self::HASH_METHOD_MD5);
            $salt = $matches[1] . $matches[2];
            return $salt;
        }

        throw new Exception('Invalid hash');
    }

    /**
     * create a hash value
     *
     * @param mixed $data
     * @return string
     */
    public function createHash($data)
    {
        $randomSalt   = $this->createRandomSalt();
        switch ($this->hashMethod)
        {
            case self::HASH_METHOD_BLOWFISH:
                $salt = '$2a$' . sprintf('%1$02d', $this->iterations) . '$';
                break;
            case self::HASH_METHOD_SHA256:
                $salt = '$5$rounds=' . $this->iterations . '$';
                break;
            case self::HASH_METHOD_SHA512:
                $salt = '$6$rounds=' . $this->iterations . '$';
                break;
            case self::HASH_METHOD_MD5:
                $salt = '$1$';
                break;
        }

        $tempSalt = str_shuffle($randomSalt);
        $salt .= substr($tempSalt,0, $this->saltLength);

        $dataString = serialize($data);
        return $this->createHashWithSalt($dataString, $salt);
    }

    /**
     * initialise hashing
     * @return void
     */
    final public function __construct()
    {
        $this->customInit();    // optionally overwrite $this->hashMethod
        $this->setHashMethod($this->hashMethod);
        $this->setIterations($this->iterations);
    }

    /**
     * set number of iterations
     * @param int $iterations
     * @return void
     */
    public function setIterations($iterations = null)
    {
        if (null !== $iterations)
        {
            // ensure iterations in correct range
            if ($iterations < $this->minIterations || $iterations > $this->maxIterations)
            {
                $iterations = null;
            }
        }
        if (null === $iterations)
        {
            $iterations = $this->getRandomIterations();
        }
        $this->iterations = $iterations;
    }

    /**
     * get random number of iterations depending on hash method
     * @return int
     */
    protected function getRandomIterations()
    {
        $iterations = rand($this->minIterations, $this->maxIterations);
        return $iterations;
    }

    /**
     * initialise hashMethod:
     * should call setHashMethod() with the method to use (or do nothing to keep default)
     * use a HASH_METHOD_ constant as parameter to setHashMethod()
     * may also overwrite  maxIterations* variables
     *
     * @return void
     */
    abstract protected function customInit();

    /**
     * check whether a particular method is supported by system
     * @param string $method
     * @return bool
     */
    protected function methodExists($method)
    {
        switch ($method)
        {
            case self::HASH_METHOD_BLOWFISH:
                if (defined(CRYPT_BLOWFISH) && CRYPT_BLOWFISH == 1)
                {
                    return true;
                }
                break;
            case self::HASH_METHOD_SHA512:
                if (defined(CRYPT_SHA512) && CRYPT_SHA512 == 1)
                {
                    return true;
                }
                break;
            case self::HASH_METHOD_SHA256:
                if (defined(CRYPT_SHA256) && CRYPT_SHA256 == 1)
                {
                    return true;
                }
                break;
            case self::HASH_METHOD_MD5:
                if (defined(CRYPT_MD5) && CRYPT_MD5 == 1)
                {
                    return true;
                }
                break;
            return false;
        }
    }

    /**
     * get best nethod supported by system
     * @return string
     */
    protected function getBestMethod()
    {
        if (defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1)
        {
            return self::HASH_METHOD_BLOWFISH;
        }
        if (defined('CRYPT_SHA512') && CRYPT_SHA512 == 1)
        {
            return self::HASH_METHOD_SHA512;
        }
        if (defined('CRYPT_SHA256') && CRYPT_SHA266 == 1)
        {
            return self::HASH_METHOD_SHA256;
        }
        if (defined('CRYPT_MD5') && CRYPT_MD5 == 1)
        {
            return self::HASH_METHOD_MD5;
        }
        throw new \Exception('No encryption method available');
    }

    /**
     * set hashMethod
     * @return void
     */
    protected function setHashMethod($method)
    {
        if (false === $this->methodExists($method))
        {
            $this->hashMethod = $this->getBestMethod();
        }
        else
        {
            $this->hashMethod = $method;
        }
        switch ($this->hashMethod)
        {
            case self::HASH_METHOD_BLOWFISH:
                $this->maxIterations = $this->maxIterationsBlowfish;
                $this->minIterations = $this->minIterationsBlowfish;
                $this->saltLength = self::SALT_LENGTH_BLOWFISH;
                break;
            case self::HASH_METHOD_SHA256:
                $this->maxIterations = $this->maxIterationsSha256;
                $this->minIterations = $this->minIterationsSha256;
                $this->saltLength = self::SALT_LENGTH_SHA256;
                break;
            case self::HASH_METHOD_SHA512:
                $this->maxIterations = $this->maxIterationsSha512;
                $this->minIterations = $this->minIterationsSha512;
                $this->saltLength = self::SALT_LENGTH_SHA512;
                break;
            case self::HASH_METHOD_MD5:
            default:
                $this->maxIterations = $this->maxIterationsMd5;
                $this->minIterations = $this->minIterationsMd5;
                $this->saltLength = self::SALT_LENGTH_MD5;
                break;
        }
    }

    /**
     * verify that a hash is correct
     *
     * @param mixed $data
     * @param string $hash
     * @throws \Exception if hash not correct
     * @return true
     */
    private function verifyHashWithSalt($data, $salt, $hash)
    {
        $dataString = serialize($data);
        $calculatedHash = $this->createHashWithSalt($dataString, $salt);
        if ($calculatedHash != $hash)
        {
            return false;
            throw new \Exception('Data could not be verified with hash.');
        }
        return true;
    }

    /**
     * create hash value
     * @param string $dataString
     * @param string $salt
     * @return string
     */
    private function createHashWithSalt($dataString, $salt)
    {
        $hash = crypt($dataString, $salt);
        return $hash;
    }

    /**
     * create a random salt
     * @return string
     */
    private function createRandomSalt()
    {
        $saltChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $randomSalt = '' ;
        $length = rand(50, 100);
        for ($i = 0; $i < $length; $i++)
        {
            $tmpStr = str_shuffle($saltChars);
            $randomSalt .= $tmpStr[0];
        }
        return $randomSalt;
    }

}

########

/**
* MyHash Class
*/
class MyHash extends AbstractHash
{
    /**
     * set the hashing  method
     * @return void
     */
    protected function customInit()
    {
        //  $this->setHashMethod(self::HASH_METHOD_SHA256);
        $this->maxIterationsBlowfish = 5;
    }
}

$data = new stdClass();
$data->param1 = 'param1';
$data->param2 = array('arr1' => 1, 'arr2' => 2);

$hasher = new MyHash();

$hash = $hasher->createHash($data);
$verify = $hasher->verifyHash($data, $hash);

// lets see what we created..
var_dump($data, $hash, $verify);
Advertisements

1 Kommentar »

  1. Hi Jonathan! This is a really great post. I’m a Content Curator at DZone and I noticed a few tweets from you about DZone. We really appreciate your readership! I’m wondering if you’ve signed up for the DZone Newsletter yet, and if you’ve heard abour our MVB program. If you’re interested in hearing more, send me an e-mail at allenc[at]dzone[dot]com. Again, thanks for your readership!

    Kommentar von Allen — 24.10.2012 @ 22:15


RSS feed for comments on this post. TrackBack URI

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

Erstelle eine kostenlose Website oder Blog – auf WordPress.com.

%d Bloggern gefällt das: