<?php
/**
 * $Horde: passwd/lib/Driver.php,v 1.31 2003/09/05 11:07:17 jan Exp $
 *
 * Passwd_Driver:: defines an API for implementing password change systems for Passwd.
 *
 * @author   Mike Cochrane <mike@graftonhall.co.nz>
 * @author   Eric Rostetter <eric.rostetter@physics.utexas.edu>
 * @version  $Revision: 1.31 $
 * @since    Passwd 2.1
 * @package  passwd
 */
class Passwd_Driver {

    /** error string returned to user if an eror occurs. */
    var $err_str;

    /**
     * Attempts to return a concrete Passwd_Driver instance based on $driver.
     *
     * @param string $driver  The type of concrete passwd_Driver subclass
     *                        to return.  The is based on the passwd
     *                        driver ($driver).  The code is dynamically
     *                        included.
     *
     * @param array  $params  (optional) A hash containing any additional
     *                        configuration or connection parameters a
     *                        subclass might need.
     *
     * @return mixed          The newly created concrete Passwd_Driver
     *                        instance, or false on an error.
     */
    function &factory($driver, $params = array())
    {
        $driver = basename($driver);
        require_once dirname(__FILE__) . '/Driver/' . $driver . '.php';
        $class = 'Passwd_Driver_' . $driver;
        if (class_exists($class)) {
            return new $class($params);
        } else {
            Horde::fatal(PEAR::raiseError(sprintf(_("No such backend '%s' found."), $driver)), __FILE__, __LINE__);
        }
    }

    /**
     * Attempts to return a reference to a concrete Passwd_Driver instance
     * based on $driver.  It will only create a new instance if no
     * Passwd_Driver instance with the same parameters currently exists.
     *
     * This should be used if multiple storage sources are required.
     *
     * This method must be invoked as: $var = &Passwd_Driver::singleton()
     *
     * @param string    $driver     The type of concrete Passwd_Driver subclass
     *                              to return.  The is based on the passwd
     *                              driver ($driver).  The code is dynamically
     *                              included.
     *
     * @param array     $params     (optional) A hash containing any additional
     *                              configuration or connection parameters a
     *                              subclass might need.
     *
     * @return mixed    The created concrete Passwd_Driver instance, or false
     *                  on error.
     */
    function &singleton($driver, $params = array())
    {
        static $instances;

        if (!isset($instances)) {
            $instances = array();
        }

        $signature = serialize(array($driver, $params));
        if (!isset($instances[$signature])) {
            $instances[$signature] = &Passwd_Driver::factory($driver, $params);
        }

        return $instances[$signature];
    }

    /**
     * Compare a plaintext password with an encrypted password.
     *
     * @return Boolean  True is they match, False if they differ
     */
    function comparePasswords($encrypted, $plaintext)
    {
        switch ($this->_params['encryption']) {
            case 'plain':
                if ($encrypted == $plaintext) {
                    return true;
                }
                break;
            case 'md5-hex':
                if ($encrypted == md5($plaintext)) {
                    return true;
                }
                break;
            case 'crypt':
            case 'crypt-des':
                $encrypted = preg_replace('|^{crypt}|', '', $encrypted);
                $salt = substr($encrypted, 0, 2);
                if ($encrypted == crypt($plaintext, $salt)) {
                    return true;
                }
                break;
            case 'crypt-md5':
                $encrypted = preg_replace('|^{crypt}|', '', $encrypted);
                $salt = substr($encrypted, 0, 12);
                if ($encrypted == crypt($plaintext, $salt)) {
                    return true;
                }
            case 'crypt-blowfish':
                $encrypted = preg_replace('|^{crypt}|', '', $encrypted);
                $salt = substr($encrypted, 0, 16);
                if ($encrypted == crypt($plaintext, $salt)) {
                    return true;
                }
                break;
            case 'sha':
                $encrypted = preg_replace('|^{SHA}|', '', $encrypted);
                if ($encrypted == base64_encode(mHash(MHASH_SHA1, $plaintext))) {
                    return true;
                }
                break;
            case 'md5-base64':
                $encrypted = preg_replace('|^{MD5}|', '', $encrypted);
                if ($encrypted == base64_encode(mHash(MHASH_MD5, $plaintext))) {
                    return true;
                }
                break;
            case 'ssha':
                $encrypted = preg_replace('|^{SSHA}|', '', $encrypted);
                $salt = substr($encrypted, -20);
                if ($encrypted == base64_encode(mHash(MHASH_SHA1, $plaintext . $salt) . $salt)) {
                    return true;
                }
                break;
            case 'smd5':
                $encrypted = preg_replace('|^{SMD5}|', '', $encrypted);
                $salt = substr($encrypted, -16);
                if ($encrypted == base64_encode(mHash(MHASH_SMD5, $plaintext . $salt) . $salt)) {
                    return true;
                }
                break;
            default:
                return PEAR::raiseError(sprintf(_("Encryption '%s' not implemented yet."), $this->_params['encryption']));
                break;
        }
        return PEAR::raiseError(_("Incorrect Password"));
    }

    /**
     * Format a password using the current encryption.
     *
     * @param  $new_password  The plaintext password to encrypt.
     *
     * @return String         The formated password.
     */
    function encryptPassword($new_password, $show_encryption = true)
    {
        /* Encrypt the password */
        switch ($this->_params['encryption']) {
            case 'plain':
                break;
            case 'md5-hex':
                $new_password = md5($new_password);
                break;
            case 'crypt':
            case 'crypt-des':
                $salt = substr(md5(mt_rand()), 0, 2);
                $new_password = crypt($new_password, $salt);
                $new_password .= ($show_encryption ? '{crypt}' : '');
                break;
            case 'crypt-md5':
                $salt = '$1$' . substr(md5(mt_rand()), 0, 8) . '$';
                $new_password = crypt($new_password, $salt);
                $new_password .= ($show_encryption ? '{crypt}' : '');
                break;
            case 'crypt-blowfish':
                $salt = '$2$' . substr(md5(mt_rand()), 0, 12) . '$';
                $new_password = crypt($new_password, $salt);
                $new_password .= ($show_encryption ? '{crypt}' : '');
                break;
            case 'sha':
                $new_password = base64_encode(mHash(MHASH_SHA1, $new_password));
                $new_password .= ($show_encryption ? '{SHA}' : '');
                break;
            case 'md5-base64':
                $new_password = base64_encode(mHash(MHASH_MD5, $new_password));
                $new_password .= ($show_encryption ? '{MD5}' : '');
                break;
            case 'ssha':
                $salt = mhash_keygen_s2k(MHASH_SHA1, $new_password, substr(pack("h*", md5(mt_rand())), 0, 8), 4);
                $new_password = base64_encode(mHash(MHASH_SHA1, $new_password . $salt) . $salt);
                $new_password .= ($show_encryption ? '{SSHA}' : '');
                break;
            case 'smd5':
                $salt = mhash_keygen_s2k(MHASH_MD5, $new_password, substr(pack("h*", md5(mt_rand())), 0, 8), 4);
                $new_password = base64_encode(mHash(MHASH_SMD5, $new_password . $salt) . $salt);
                $new_password .= ($show_encryption ? '{SMD5}' : '');
                break;
            default:
                return PEAR::raiseError(_("Password module is not properly configured."));
                break;
        }
        return $new_password;
    }

    /**
     * Change the user's password.
     *
     * @param   $username     The user for which to change the password.
     * @param   $oldpassword  The old (current) user password.
     * @param   $new_password  The new user password to set.
     *
     * @return  boolean       True or false based on success of the change.
     */
    function changePassword($username, $oldpassword, $new_password)
    {
        return PEAR::raiseError(_("Backend not correctly implemented."));
    }

    /**
     * Change the Horde/IMP cached credentials.  Should be called only
     * after a successful change of the password in the actual backend
     * storage.  This routine is the same for all backends and should
     * not be implemented in the backend classes.
     *
     * @param   $username     The username we're changing.
     * @param   $oldpassword  The old user password.
     * @param   $new_password  The new user password to set.
     *
     * @return  none
     */
    function resetCredentials($username, $oldpassword, $new_password)
    {
        if ((Auth::getBareAuth() == $username) &&
            (Auth::getCredential('password') == $oldpassword)) {
            $credentials['transparent'] = Auth::getCredential('transparent');
            $credentials['password'] = $new_password;
            Auth::setAuth(Auth::getAuth(), $credentials);
            if (Auth::getProvider() == 'imp') {
                $_SESSION['imp']['pass'] = Secret::write(Secret::getKey('imp'),
                                                         $new_password);
            }
        }
    }

}
