Ignore:
Timestamp:
Nov 15, 2014 9:34:39 PM (10 years ago)
Author:
anonymous
Message:

Many auth and crypto changes; various other bugfixes while working on pulso.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/lib/Auth_SQL.inc.php

    r497 r500  
    3939    const ENCRYPT_MD5 = 5;
    4040    const ENCRYPT_MD5_HARDENED = 6;
     41    const ENCRYPT_PASSWORD_BCRYPT = 7;
     42    const ENCRYPT_PASSWORD_DEFAULT = 8;
    4143
    4244    // Namespace of this auth object.
     
    174176                " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
    175177                userpass VARCHAR(255) NOT NULL DEFAULT '',
     178                userpass_hashtype TINYINT UNSIGNED NOT NULL DEFAULT '0',
    176179                first_name VARCHAR(255) NOT NULL DEFAULT '',
    177180                last_name VARCHAR(255) NOT NULL DEFAULT '',
     
    260263            $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']);
    261264        }
     265        if (isset($params['encryption_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['encryption_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
     266            // These hash types require the password_* userland lib in PHP < 5.5.0
     267            $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php';
     268            if (false !== stream_resolve_include_path($pw_compat_lib)) {
     269                include_once $pw_compat_lib;
     270            } else {
     271                $app =& App::getInstance();
     272                $app->logMsg(sprintf('Encryption type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1', $params['encryption_type']), LOG_ERR, __FILE__, __LINE__);
     273                $params['encryption_type'] = self::ENCRYPT_SHA1;
     274            }
     275        }
    262276        if (isset($params) && is_array($params)) {
    263277            // Merge new parameters with old overriding only those passed.
     
    353367
    354368    /**
    355      * Find out if a set of login credentials are valid.
     369     * Retrieve and verify the given username and password against a matching user record in the database.
    356370     *
    357371     * @access private
     
    367381        $this->initDB();
    368382
    369         switch ($this->_params['encryption_type']) {
    370         case self::ENCRYPT_CRYPT :
    371             // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
    372             $qid = $db->query("
    373                 SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
    374                 FROM " . $this->_params['db_table'] . "
    375                 WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
    376                 AND BINARY userpass = ENCRYPT('" . $db->escapeString($password) . "', LEFT(userpass, 2)))
    377             ");
    378             break;
    379         case self::ENCRYPT_PLAINTEXT :
    380         case self::ENCRYPT_MD5 :
    381         case self::ENCRYPT_SHA1 :
    382         default :
    383             // Query DB for user matching credentials. Directly compare cyphertext with result from encryptPassword().
    384             $qid = $db->query("
    385                 SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
    386                 FROM " . $this->_params['db_table'] . "
    387                 WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
    388                 AND BINARY userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
    389             ");
    390             break;
    391         }
    392 
    393         // Return user data if found.
    394         if ($user_data = mysql_fetch_assoc($qid)) {
    395             // Don't return password value.
    396             unset($user_data['userpass']);
    397             $app->logMsg(sprintf('Authentication successful for user_id %s (%s)', $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__);
     383        // Get user data for specified username.
     384        // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
     385        $qid = $db->query("
     386            SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
     387            FROM " . $this->_params['db_table'] . "
     388            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
     389        ");
     390        if (!$user_data = mysql_fetch_assoc($qid)) {
     391            $app->logMsg(sprintf('Username %s not found for authentication', $username), LOG_NOTICE, __FILE__, __LINE__);
     392            return false;
     393        }
     394
     395        if ($this->verifyPassword($password, $user_data['userpass'])) {
     396            $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__);
     397            unset($user_data['userpass']); // Avoid revealing the encrypted password in the $user_data.
    398398            return $user_data;
    399         } else {
    400             $app->logMsg(sprintf('Authentication failed for username %s (encrypted attempted password: %s)', $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__);
    401             return false;
    402         }
     399        }
     400
     401        $app->logMsg(sprintf('Authentication failed for %s (user_id=%s)', $username, $user_data['user_id']), LOG_NOTICE, __FILE__, __LINE__);
     402        return false;
    403403    }
    404404
     
    553553                SELECT 1 FROM " . $this->_params['db_table'] . "
    554554                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
    555                 AND DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW()
    556                 AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW()
     555                AND last_login_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['login_timeout'] . "' SECOND)
     556                AND last_access_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND)
    557557            ");
    558558            $login_status = (mysql_num_rows($qid) > 0);
     
    809809    }
    810810
    811     /**
    812      * Returns a randomly generated password based on $pattern. The pattern is any
    813      * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this
    814      * function will generate a pronounceable password. Recommend using more complex
    815      * patterns, at minimum the US State Department standard: cvcddcvc.
    816      *
    817      * - x    A random upper or lower character, digit, or punctuation.
    818      * - C    A random upper or lower consonant.
    819      * - V    A random upper or lower vowel.
    820      * - c    A random lowercase consonant.
    821      * - v    A random lowercase vowel.
    822      * - d    A random digit.
    823      *
    824      * @param  string $pattern  a sequence of character types, above.
    825      * @return string           a password
    826      */
    827     public function generatePassword($pattern='CvcdCvc')
    828     {
    829         $app =& App::getInstance();
    830         if (preg_match('/[^xCVcvd]/', $pattern)) {
    831             $app->logMsg(sprintf('Invalid pattern: %s', $pattern), LOG_WARNING, __FILE__, __LINE__);
    832             $pattern='CvcdCvc';
    833         }
    834         $str = '';
    835         for ($i=0; $i<mb_strlen($pattern); $i++) {
    836             $x = mb_substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789!@#%&*-=+.?', (mt_rand() % 71), 1);
    837             $c = mb_substr('bcdfghjklmnprstvwxz', (mt_rand() % 19), 1);
    838             $C = mb_substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ', (mt_rand() % 38), 1);
    839             $v = mb_substr('aeiouy', (mt_rand() % 6), 1);
    840             $V = mb_substr('aeiouyAEIOUY', (mt_rand() % 12), 1);
    841             $d = mb_substr('0123456789', (mt_rand() % 10), 1);
    842             $str .= $$pattern[$i];
    843         }
    844         return $str;
     811    /*
     812    * Generate a cryptographically secure, random password.
     813    *
     814    * @access   public
     815    * @param    int  $bytes     Length of password (in bytes)
     816    * @return   string          Random string of characters.
     817    * @author   Quinn Comendant <quinn@strangecode.com>
     818    * @version  1.0
     819    * @since    15 Nov 2014 20:30:27
     820    */
     821    public function generatePassword($bytes=10)
     822    {
     823        $app =& App::getInstance();
     824
     825        $bytes = is_numeric($bytes) ? $bytes : 10;
     826        $string = strtok(base64_encode(openssl_random_pseudo_bytes($bytes, $strong)), '=');
     827        if (!$strong) {
     828            $app->logMsg(sprintf('Password generated was not "cryptographically strong"; check your openssl.', null), LOG_NOTICE, __FILE__, __LINE__);
     829        }
     830
     831        return $string;
    845832    }
    846833
     
    851838    {
    852839        $app =& App::getInstance();
     840
     841        $password = (string)$password;
    853842
    854843        // Existing password hashes rely on the same key/salt being used to compare encryptions.
     
    858847        switch ($this->_params['encryption_type']) {
    859848        case self::ENCRYPT_PLAINTEXT :
    860             return $password;
     849            $encrypted_password = $password;
    861850            break;
    862851
    863852        case self::ENCRYPT_CRYPT :
    864             // If comparing plaintext password with a hash, provide first two chars of the hash as the salt.
    865             return isset($salt) ? crypt($password, mb_substr($salt, 0, 2)) : crypt($password);
     853            // If comparing password with an existing hashed password, provide the hashed password as the salt.
     854            $encrypted_password = isset($salt) ? crypt($password, $salt) : crypt($password);
    866855            break;
    867856
    868857        case self::ENCRYPT_SHA1 :
    869             return sha1($password);
     858            $encrypted_password = sha1($password);
    870859            break;
    871860
    872861        case self::ENCRYPT_SHA1_HARDENED :
    873             $hash = sha1($app->getParam('signing_key') . $password . $more_salt);
    874             // Increase key strength by 12 bits.
    875             for ($i=0; $i < 4096; $i++) {
    876                 $hash = sha1($hash);
    877             }
    878             return $hash;
     862            $encrypted_password = sha1($app->getParam('signing_key') . $password . $more_salt);
     863            for ($i=0; $i < pow(2, 20); $i++) {
     864                $encrypted_password = sha1($password . $encrypted_password);
     865            }
    879866            break;
    880867
    881868        case self::ENCRYPT_MD5 :
    882             return md5($password);
     869            $encrypted_password = md5($password);
    883870            break;
    884871
    885872        case self::ENCRYPT_MD5_HARDENED :
    886             // Include salt to improve hash
    887             $hash = md5($app->getParam('signing_key') . $password . $more_salt);
    888             // Increase key strength by 12 bits.
    889             for ($i=0; $i < 4096; $i++) {
    890                 $hash = md5($hash);
    891             }
    892             return $hash;
     873            $encrypted_password = md5($app->getParam('signing_key') . $password . $more_salt);
     874            for ($i=0; $i < pow(2, 20); $i++) {
     875                $encrypted_password = md5($password . $encrypted_password);
     876            }
     877            break;
     878
     879        case self::ENCRYPT_PASSWORD_BCRYPT :
     880            $encrypted_password = password_hash($password, PASSWORD_BCRYPT, array('cost' => 12));
     881            break;
     882
     883        case self::ENCRYPT_PASSWORD_DEFAULT :
     884            $encrypted_password = password_hash($password, PASSWORD_DEFAULT, array('cost' => 12));
    893885            break;
    894886
     
    897889            return false;
    898890            break;
     891        }
     892
     893        // In case our hashing function returns 'false' or another empty value, bail out.
     894        if ('' == trim((string)$encrypted_password)) {
     895            $app->logMsg(sprintf('Invalid password hash returned; check yo crypto!', null), LOG_ALERT, __FILE__, __LINE__);
     896            return false;
     897        }
     898
     899        return $encrypted_password;
     900    }
     901
     902    /*
     903    *
     904    *
     905    * @access   public
     906    * @param
     907    * @return
     908    * @author   Quinn Comendant <quinn@strangecode.com>
     909    * @version  1.0
     910    * @since    15 Nov 2014 21:37:28
     911    */
     912    public function verifyPassword($password, $encrypted_password)
     913    {
     914        switch ($this->_params['encryption_type']) {
     915        case self::ENCRYPT_CRYPT :
     916            return $this->encryptPassword($password, $encrypted_password) == $encrypted_password;
     917
     918        case self::ENCRYPT_PLAINTEXT :
     919        case self::ENCRYPT_MD5 :
     920        case self::ENCRYPT_MD5_HARDENED :
     921        case self::ENCRYPT_SHA1 :
     922        case self::ENCRYPT_SHA1_HARDENED :
     923        default :
     924            return $this->encryptPassword($password) == $encrypted_password;
     925
     926        case self::ENCRYPT_PASSWORD_BCRYPT :
     927        case self::ENCRYPT_PASSWORD_DEFAULT :
     928            return password_verify($password, $encrypted_password);
    899929        }
    900930    }
     
    920950        ");
    921951        if (!list($old_encrypted_password) = mysql_fetch_row($qid)) {
    922             $app->logMsg(sprintf('Cannot set password for nonexistent user_id %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
     952            $app->logMsg(sprintf('Cannot set password for nonexistent user_id %s', $user_id), LOG_WARNING, __FILE__, __LINE__);
    923953            return false;
    924954        }
    925955
    926956        // Compare old with new to ensure we're actually *changing* the password.
    927         $encrypted_password = $this->encryptPassword($password);
    928         if ($old_encrypted_password == $encrypted_password) {
     957        if ($this->verifyPassword($password, $old_encrypted_password)) {
    929958            $app->logMsg(sprintf('Not setting password: new is the same as old.', null), LOG_INFO, __FILE__, __LINE__);
    930             return false;
     959            return null;
     960        }
     961
     962        // Save the hash method used if a table exists for it.
     963        $userpass_hashtype = '';
     964        if ($db->columnExists($this->_params['db_table'], 'userpass_hashtype', false)) {
     965            $userpass_hashtype = ", userpass_hashtype = '" . $db->escapeString($this->getParam('encryption_type')) . "'";
    931966        }
    932967
     
    934969        $db->query("
    935970            UPDATE " . $this->_params['db_table'] . "
    936             SET userpass = '" . $db->escapeString($encrypted_password) . "'
     971            SET userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
     972            $userpass_hashtype
    937973            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
    938974        ");
     
    943979        }
    944980
     981        $app->logMsg(sprintf('Password change successful for user_id %s', $user_id), LOG_INFO, __FILE__, __LINE__);
    945982        return true;
    946983    }
     
    10081045    }
    10091046
    1010     /**
    1011      * If the current user has access to the specified $security_zone, return true.
    1012      * If the optional $user_type is supplied, test that against the zone.
    1013      *
    1014      * NOTE: "user_type" used to be called "priv" in some older implementations.
    1015      *
    1016      * @param  constant $security_zone   string of comma delimited privileges for the zone
    1017      * @param  string   $user_type       a privilege that might be found in a zone
    1018      * @return bool     true if user is a member of security zone, false otherwise
    1019      */
    1020     public function inClearanceZone($security_zone, $user_type='')
    1021     {
    1022         $zone_members = preg_split('/,\s*/', $security_zone);
    1023         $user_type = empty($user_type) ? $this->get('user_type') : $user_type;
    1024 
    1025         // If the current user's privilege level is NOT in that array or if the
    1026         // user has no privilege, return false. Otherwise the user is clear.
    1027         if (!in_array($user_type, $zone_members) || empty($user_type)) {
    1028             return false;
    1029         } else {
    1030             return true;
    1031         }
    1032     }
    1033 
    1034     /**
    1035      * This function tests a list of arguments $security_zone against the priv that the current user has.
    1036      * If the user doesn't have one of the supplied privs, die.
    1037      *
    1038      * NOTE: "user_type" used to be called "priv" in some older implementations.
    1039      *
    1040      * @param  constant $security_zone   string of comma delimited privileges for the zone
    1041      */
    1042     public function requireAccessClearance($security_zone, $message='')
    1043     {
    1044         $app =& App::getInstance();
    1045 
    1046         $zone_members = preg_split('/,\s*/', $security_zone);
    1047 
    1048         /* If the current user's privilege level is NOT in that array or if the
    1049          * user has no privilege, DIE with a message. */
    1050         if (!in_array($this->get('user_type'), $zone_members) || !$this->get('user_type')) {
    1051             $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message;
    1052             $app->raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__);
    1053             $app->dieBoomerangURL();
    1054         }
    1055     }
    1056 
    10571047} // end class
    1058 
    1059 // CIDR cheat-sheet
    1060 //
    1061 // Netmask              Netmask (binary)                 CIDR     Notes
    1062 // _____________________________________________________________________________
    1063 // 255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
    1064 // 255.255.255.254  11111111.11111111.11111111.11111110  /31  Unusable
    1065 // 255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
    1066 // 255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
    1067 // 255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
    1068 // 255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
    1069 // 255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
    1070 // 255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
    1071 // 255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable
    1072 //
    1073 // 255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
    1074 // 255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
    1075 // 255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
    1076 // 255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
    1077 // 255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
    1078 // 255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
    1079 // 255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
    1080 // 255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
    1081 //
    1082 // 255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
    1083 // 255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
    1084 // 255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
    1085 // 255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
    1086 // 255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
    1087 // 255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
    1088 // 255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
    1089 // 255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
    1090 //
    1091 // 254.0.0.0        11111110.00000000.00000000.00000000  /7
    1092 // 252.0.0.0        11111100.00000000.00000000.00000000  /6
    1093 // 248.0.0.0        11111000.00000000.00000000.00000000  /5
    1094 // 240.0.0.0        11110000.00000000.00000000.00000000  /4
    1095 // 224.0.0.0        11100000.00000000.00000000.00000000  /3
    1096 // 192.0.0.0        11000000.00000000.00000000.00000000  /2
    1097 // 128.0.0.0        10000000.00000000.00000000.00000000  /1
    1098 // 0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
Note: See TracChangeset for help on using the changeset viewer.