e107inc/e107

View on GitHub
e107_plugins/alt_auth/extended_password_handler.php

Summary

Maintainability
A
1 hr
Test Coverage
F
0%
<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2013 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 *    Extended password handler for alt_auth plugin
 *
 * $URL$
 * $Id$
 */

/**
 *    e107 Alternate authorisation plugin
 *
 *    @package    e107_plugins
 *    @subpackage    alt_auth
 *    @version     $Id$;
 */

/**
EXTENDED PASSWORD HANDLER CLASS 
    - supports many password formats used on other systems
    - implements checking of existing passwords only

To use:
    Instantiate ExtendedPasswordHandler
    call CheckPassword(plaintext_password,login_name, stored_value)
or, optionally:
    call CheckPassword(plaintext_password,login_name, stored_value, password_type)

@todo:
    1. Check that public/private declarations of functions are correct
*/


if (!defined('e107_INIT')) { exit; }


require_once(e_HANDLER.'user_handler.php');


// @todo make these class constants
/*define('PASSWORD_PHPBB_SALT',2);
define('PASSWORD_MAMBO_SALT',3);
define('PASSWORD_JOOMLA_SALT',4);
define('PASSWORD_GENERAL_MD5',5);
define('PASSWORD_PLAINTEXT',6);
define('PASSWORD_GENERAL_SHA1',7);
define('PASSWORD_WORDPRESS_SALT', 8);
define('PASSWORD_MAGENTO_SALT', 9);
define('PASSWORD_PHPFUSION_SHA256', 10);

// Supported formats:
define('PASSWORD_PHPBB_ID', '$H$');                // PHPBB salted
define('PASSWORD_ORIG_ID', '$P$');                // 'Original' code
define('PASSWORD_WORDPRESS_ID', '$P$');            // WordPress 2.8
*/



class ExtendedPasswordHandler extends UserHandler
{
    private $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';    // Holds a string of 64 characters for base64 conversion
    var $random_state = '';                        // A (hopefully) random number

    const   PASSWORD_E107_MD5       = 0;
    const   PASSWORD_E107_SALT      = 1;
    const   PASSWORD_PHPBB_SALT     = 2;
    const   PASSWORD_MAMBO_SALT     = 3;
    const   PASSWORD_JOOMLA_SALT    = 4;
    const   PASSWORD_GENERAL_MD5    = 5;
    const   PASSWORD_PLAINTEXT      = 6;
    const   PASSWORD_GENERAL_SHA1   = 7;
    const   PASSWORD_WORDPRESS_SALT = 8;
    const   PASSWORD_MAGENTO_SALT   = 9;
    const   PASSWORD_PHPFUSION_SALT = 10;
    const   PASSWORD_ABANTECART_SALT = 11;
    
    const   PASSWORD_PHPBB_ID           = '$H$';            // PHPBB salted
    const   PASSWORD_ORIG_ID            = '$P$';                // 'Original' code
    const   PASSWORD_WORDPRESS_ID       = '$P$';            // WordPress 2.8

    /**
     * Constructor - just call parent
     */
    function __construct()
    {
        // Ancestor constructor
          parent::__construct();
    }


    /**
     *    Return a number of random bytes as specified by $count
     */
    private function get_random_bytes($count)
    {
        $this->random_state = md5($this->random_state.microtime().random_int(0,10000));  // This will 'auto seed'

        $output = '';
        for ($i = 0; $i < $count; $i += 16) 
        {    // Only do this loop once unless we need more than 16 bytes
          $this->random_state = md5(microtime() . $this->random_state);
          $output .= pack('H*', md5($this->random_state));        // Becomes an array of 16 bytes
        }
        $output = substr($output, 0, $count);

        return $output;
    }


    /**
     *     Encode to base64 (each block of three 8-bit chars becomes 4 printable chars)
     *    Use first $count characters of $input string
     */
    private function encode64($input, $count)
    {
        return base64_encode(substr($input, 0, $count));    // @todo - check this works OK
        /*
        $output = '';
        $i = 0;
        do 
        {
          $value = ord($input[$i++]);    
          $output .= $this->itoa64[$value & 0x3f];
          if ($i < $count) $value |= ord($input[$i]) << 8;
          $output .= $this->itoa64[($value >> 6) & 0x3f];
          if ($i++ >= $count) break;
          if ($i < $count) $value |= ord($input[$i]) << 16;
          $output .= $this->itoa64[($value >> 12) & 0x3f];
          if ($i++ >= $count) break;
          $output .= $this->itoa64[($value >> 18) & 0x3f];
        } while ($i < $count);

        return $output;
        */
    }



    /**
     *    Method for PHPBB3-style salted passwords, which begin '$H$', and WordPress-style salted passwords, which begin '$P$'
     *    Given a plaintext password and the complete password/hash function (which includes any salt), calculate hash
     *    Returns FALSE on error
     */
    private function crypt_private($password, $stored_password, $password_type = self::PASSWORD_PHPBB_SALT)
    {
        $output = '*0';
        if (substr($stored_password, 0, 2) == $output)
        {
            $output = '*1';
        }

        $prefix = '';
        switch ($password_type)
        {
            case self::PASSWORD_PHPBB_SALT :
                $prefix = self::PASSWORD_PHPBB_ID;
                break;
            case self::PASSWORD_WORDPRESS_SALT :
                $prefix = self::PASSWORD_WORDPRESS_ID;
                break;
            default :
                $prefix = '';
        }

        if ($prefix != substr($stored_password, 0, 3))
        {
            return $output;
        }

        $count_log2 = strpos($this->itoa64, $stored_password[3]);            // 4th character indicates hash depth count
        if ($count_log2 < 7 || $count_log2 > 30)
        {
            return $output;
        }

        $count = 1 << $count_log2;

        $salt = substr($stored_password, 4, 8);                        // Salt is characters 5..12
        if (strlen($salt) != 8)
        {
            return $output;
        }

        # We're kind of forced to use MD5 here since it's the only
        # cryptographic primitive available in all versions of PHP
        # currently in use.  To implement our own low-level crypto
        # in PHP would result in much worse performance and
        # consequently in lower iteration counts and hashes that are
        # quicker to crack (by non-PHP code).
        // Get raw binary output (always 16 bytes) - we assume PHP5 here
        $hash = md5($salt.$password, TRUE);
        do 
        {
            $hash = md5($hash.$password, TRUE);
        } while (--$count);

        $output = substr($setting, 0, 12);        // Identifier, shift count and salt - total 12 chars
        $output .= $this->encode64($hash, 16);    // Returns 22-character string

        return $output;
    }


    /**
     *    Return array of supported password types - key is used internally, text is displayed
     */
    public function getPasswordTypes($includeExtended = TRUE)
    {
        $vals = array( // Methods supported in core
            'md5' => IMPORTDB_LAN_7,
            'e107_salt' => IMPORTDB_LAN_8
        );

        if ($includeExtended)
        {
            $vals = array_merge($vals,array( 
                'plaintext'         => IMPORTDB_LAN_2, 
                'joomla_salt'        => IMPORTDB_LAN_3, 
                'mambo_salt'        => IMPORTDB_LAN_4,
                'smf_sha1'            => IMPORTDB_LAN_5,
                'sha1'                => IMPORTDB_LAN_6,
                'phpbb3_salt'        => IMPORTDB_LAN_12,
                'wordpress_salt'    => IMPORTDB_LAN_13,
                'magento_salt'        => IMPORTDB_LAN_14,
                'phpfusion_salt'    => "PHPFusion",
                'abantecart_salt'    => "AbanteCart Salt",
                ));
        }
        return $vals;
    }


    /**
     *    Return password type which relates to a specific foreign system
     */
    public function passwordMapping($ptype)
    {
        $maps = array( 
                'plaintext'         => self::PASSWORD_PLAINTEXT,
                'joomla_salt'         => self::PASSWORD_JOOMLA_SALT,
                'mambo_salt'         => self::PASSWORD_MAMBO_SALT,
                'smf_sha1'             => self::PASSWORD_GENERAL_SHA1,
                'sha1'                 => self::PASSWORD_GENERAL_SHA1,
                'mambo'             => self::PASSWORD_GENERAL_MD5,
                'phpbb2'            => self::PASSWORD_GENERAL_MD5,
                'e107'                => self::PASSWORD_GENERAL_MD5,
                'md5'                => self::PASSWORD_GENERAL_MD5,
                'e107_salt'            => self::PASSWORD_E107_SALT,
                'phpbb2_salt'        => self::PASSWORD_PHPBB_SALT,
                'phpbb3_salt'        => self::PASSWORD_PHPBB_SALT,
                'wordpress_salt'    => self::PASSWORD_WORDPRESS_SALT,
                'magento_salt'        => self::PASSWORD_MAGENTO_SALT,
                'phpfusion_salt'    => self::PASSWORD_PHPFUSION_SALT,
                'abantecart_salt'   => self::PASSWORD_ABANTECART_SALT,
                );
        if (isset($maps[$ptype])) return $maps[$ptype];
        return FALSE;
    }


    /**
     *    Extension of password validation to handle more types
     *
     *    @param string $password - plaintext password as entered by user
     *    @param string $login_name - string used to log in (could actually be email address)
     *    @param string $stored_hash - required value for password to match
     *    @param integer $password_type - constant specifying the type of password to check against
     *
     *    @return string  PASSWORD_INVALID|PASSWORD_VALID|string
     *        PASSWORD_INVALID if no match
     *        PASSWORD_VALID if valid password
     *        Return a new hash to store if valid password but non-preferred encoding
     */
    public function CheckPassword($password, $login_name, $stored_hash, $password_type = PASSWORD_DEFAULT_TYPE)
    {
        switch ($password_type)
        {
            case self::PASSWORD_GENERAL_MD5 :
            case self::PASSWORD_E107_MD5 :
                $pwHash = md5($password);

                break;

            case self::PASSWORD_GENERAL_SHA1 :
                if (strlen($stored_hash) != 40) return PASSWORD_INVALID;
                $pwHash = sha1($password);
                break;

            case self::PASSWORD_JOOMLA_SALT :
            case self::PASSWORD_MAMBO_SALT :
                if ((strpos($stored_hash, ':') === false) || (strlen($stored_hash) < 40))
                {
                    return PASSWORD_INVALID;
                }
                // Mambo/Joomla salted hash - should be 32-character md5 hash, ':', 16-character salt (but could be 8-char salt, maybe)
                list($hash, $salt) = explode(':', $stored_hash);
                $pwHash = md5($password.$salt);
                $stored_hash = $hash;
                break;
                

            case self::PASSWORD_MAGENTO_SALT :
                $hash = $salt = '';
                if ((strpos($stored_hash, ':') !== false))
                {
                    list($hash, $salt) = explode(':', $stored_hash); 
                }
                // Magento salted hash - should be 32-character md5 hash, ':', 2-character salt, but could be also only md5 hash
                else 
                {
                    $hash = $stored_hash;
                } 
            //    if(strlen($hash) !== 32)
            //    {
                    //return PASSWORD_INVALID;
            //    }
                
                $pwHash = $salt ? md5($salt.$password) : md5($password);
                $stored_hash = $hash;
                break;

            case self::PASSWORD_E107_SALT :
                //return e107::getUserSession()->CheckPassword($password, $login_name, $stored_hash);
                return parent::CheckPassword($password, $login_name, $stored_hash);
            //    break;

            case self::PASSWORD_PHPBB_SALT :
            case self::PASSWORD_WORDPRESS_SALT :
                if (strlen($stored_hash) != 34) return PASSWORD_INVALID;
                $pwHash = $this->crypt_private($password, $stored_hash, $password_type);
                if ($pwHash[0] === '*')
                {
                    return PASSWORD_INVALID;
                }
                $stored_hash = substr($stored_hash,12);
                break;

            case self::PASSWORD_PHPFUSION_SALT:

                list($hash, $salt) = explode(':', $stored_hash);

                if (strlen($hash) !== 32)
                {
                    $pwHash = hash_hmac('sha256',$password, $salt);
                }
                else
                {
                    e107::getMessage()->addDebug("PHPFusion Md5 Hash Detected ");
                    $pwHash = md5(md5($password));
                }

                $stored_hash = $hash;
                break;

            case self::PASSWORD_PLAINTEXT :
                $pwHash = $password;
                break;
                
             
             case self::PASSWORD_ABANTECART_SALT :
                $hash = $salt = '';
                
                if ((strpos($stored_hash, ':') !== false))
                {
                    list($hash, $salt) = explode(':', $stored_hash); 
                }
                // Magento salted hash - should be 32-character md5 hash, ':', 2-character salt, but could be also only md5 hash
                else 
                {
                    $hash = $stored_hash;
                } 
                          
                /*
                password =     SHA1(CONCAT(salt, SHA1(CONCAT(salt, SHA1('".$this->db->escape($password)."')))
                */
                //$pwHash = $salt ? md5($salt.$pword) : md5($pword);
                $pwHash = sha1($salt.sha1($salt.sha1($password)));
                $stored_hash = $hash;
                break;               
                

            default :
                return PASSWORD_INVALID;
        }
       
        if(deftrue('ADMIN_AREA'))
        {
            e107::getMessage()->addDebug("Stored Hash: ".$stored_hash);

            if(!empty($salt))
            {
                e107::getMessage()->addDebug("Stored Salt: ".$salt);
            }

            e107::getMessage()->addDebug("Generated Hash: ".$pwHash);
        }

        if ($stored_hash != $pwHash) return PASSWORD_INVALID;

        return PASSWORD_VALID;
    }

}