| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Change password PearDB backend |
| 5 | * |
| 6 | * @copyright 2005-2017 The SquirrelMail Project Team |
| 7 | * @license http://opensource.org/licenses/gpl-license.php GNU Public License |
| 8 | * @version $Id$ |
| 9 | * @package plugins |
| 10 | * @subpackage change_password |
| 11 | */ |
| 12 | |
| 13 | /** load Pear DB. |
| 14 | * Global is needed because library must be loaded before configuration |
| 15 | * in order to use DB constants. |
| 16 | */ |
| 17 | global $cpw_peardb_detect; |
| 18 | $cpw_peardb_detect=@include_once('DB.php'); |
| 19 | |
| 20 | /** declare configuration globals */ |
| 21 | global $cpw_peardb_dsn, $cpw_peardb_connect_opts, $cpw_peardb_table, |
| 22 | $cpw_peardb_uid_field, $cpw_peardb_domain_field, $cpw_peardb_passwd_field, |
| 23 | $cpw_peardb_crypted_passwd, $cpw_peardb_debug; |
| 24 | |
| 25 | /** |
| 26 | * Connection DSN. |
| 27 | * Any format supported by peardb |
| 28 | * @global mixed $cpw_peardb_dsn |
| 29 | */ |
| 30 | $cpw_peardb_dsn=''; |
| 31 | |
| 32 | /** |
| 33 | * Pear DB connection options |
| 34 | * @global array $cpw_peardb_connect_opts |
| 35 | */ |
| 36 | $cpw_peardb_connect_opts=array(); |
| 37 | |
| 38 | /** |
| 39 | * Table that stores user information |
| 40 | * @global string $cpw_peardb_table |
| 41 | */ |
| 42 | $cpw_peardb_table=''; |
| 43 | |
| 44 | /** |
| 45 | * Field that stores user name |
| 46 | * @global string $cpw_peardb_uid_field |
| 47 | */ |
| 48 | $cpw_peardb_uid_field='userid'; |
| 49 | |
| 50 | /** |
| 51 | * Field that stores domain part of username |
| 52 | * @global string $cpw_peardb_domain_field |
| 53 | */ |
| 54 | $cpw_peardb_domain_field=''; |
| 55 | |
| 56 | /** |
| 57 | * Field that stores password |
| 58 | * @global string $cpw_peardb_passwd_field |
| 59 | */ |
| 60 | $cpw_peardb_passwd_field='password'; |
| 61 | |
| 62 | /** |
| 63 | * Passwords are plaintext or encrypted |
| 64 | * @global boolean $cpw_peardb_crypted_passwd |
| 65 | */ |
| 66 | $cpw_peardb_crypted_passwd=false; |
| 67 | |
| 68 | /** |
| 69 | * Controls output debugging errors |
| 70 | * Error messages might contain login and password information. |
| 71 | * Don't enable on production systems. |
| 72 | * @global boolean $cpw_peardb_debug |
| 73 | */ |
| 74 | $cpw_peardb_debug=false; |
| 75 | |
| 76 | /** configuration overrides */ |
| 77 | if ( isset($cpw_peardb) && is_array($cpw_peardb) && !empty($cpw_peardb) ) { |
| 78 | if (isset($cpw_peardb['dsn'])) |
| 79 | $cpw_peardb_dsn=$cpw_peardb['dsn']; |
| 80 | if (isset($cpw_peardb['connect_opts'])) |
| 81 | $cpw_peardb_connect_opts=$cpw_peardb['connect_opts']; |
| 82 | if (isset($cpw_peardb['table'])) |
| 83 | $cpw_peardb_table=$cpw_peardb['table']; |
| 84 | if (isset($cpw_peardb['uid_field'])) |
| 85 | $cpw_peardb_uid_field=$cpw_peardb['uid_field']; |
| 86 | if (isset($cpw_peardb['domain_field'])) |
| 87 | $cpw_peardb_domain_field=$cpw_peardb['domain_field']; |
| 88 | if (isset($cpw_peardb['password_field'])) |
| 89 | $cpw_peardb_passwd_field=$cpw_peardb['password_field']; |
| 90 | if (isset($cpw_peardb['crypted_passwd'])) |
| 91 | $cpw_peardb_crypted_passwd=true; |
| 92 | if (isset($cpw_peardb['debug'])) |
| 93 | $cpw_peardb_debug=$cpw_peardb['debug']; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Define here the name of your password changing function. |
| 98 | */ |
| 99 | global $squirrelmail_plugin_hooks; |
| 100 | $squirrelmail_plugin_hooks['change_password_dochange']['peardb'] = |
| 101 | 'cpw_peardb_dochange'; |
| 102 | $squirrelmail_plugin_hooks['change_password_init']['peardb'] = |
| 103 | 'cpw_peardb_init'; |
| 104 | |
| 105 | /** |
| 106 | * Checks if configuration is correct |
| 107 | */ |
| 108 | function cpw_peardb_init() { |
| 109 | global $oTemplate, $cpw_peardb_detect, $cpw_peardb_dsn, $cpw_peardb_table; |
| 110 | |
| 111 | if (! $cpw_peardb_detect) { |
| 112 | error_box(_("Plugin is unable to use PHP Pear DB libraries. PHP Pear includes must be available in your PHP include_path setting.")); |
| 113 | $oTemplate->display('footer.tpl'); |
| 114 | exit(); |
| 115 | } |
| 116 | |
| 117 | // Test required settings |
| 118 | if ((is_string($cpw_peardb_dsn) && trim($cpw_peardb_dsn)=='') |
| 119 | || trim($cpw_peardb_table)=='' ) { |
| 120 | error_box(_("Required change password backend configuration options are missing.")); |
| 121 | $oTemplate->display('footer.tpl'); |
| 122 | exit(); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | |
| 127 | /** |
| 128 | * Changes password |
| 129 | * @param array data The username/curpw/newpw data. |
| 130 | * @return array Array of error messages. |
| 131 | */ |
| 132 | function cpw_peardb_dochange($data) { |
| 133 | global $cpw_peardb_dsn, $cpw_peardb_table, $cpw_peardb_connect_opts, $cpw_peardb_debug, |
| 134 | $cpw_peardb_uid_field, $cpw_peardb_passwd_field, $cpw_peardb_domain_field, |
| 135 | $cpw_peardb_crypted_passwd, $domain; |
| 136 | |
| 137 | $username = $data['username']; |
| 138 | $curpw = $data['curpw']; |
| 139 | $newpw = $data['newpw']; |
| 140 | |
| 141 | $msgs = array(); |
| 142 | |
| 143 | // split user and domain parts from username, if domain field is set and username looks like email. |
| 144 | if ($cpw_peardb_domain_field!='' && preg_match("/(.*)@(.*)/",$username,$match)) { |
| 145 | $user=$match[1]; |
| 146 | $user_domain=$match[2]; |
| 147 | } else { |
| 148 | $user=$username; |
| 149 | $user_domain=$domain; |
| 150 | } |
| 151 | |
| 152 | // connect to database and make sure that table exists |
| 153 | $cpw_db = DB::connect($cpw_peardb_dsn, $cpw_peardb_connect_opts); |
| 154 | if (PEAR::isError($cpw_db)) { |
| 155 | array_push($msgs,sprintf(_("Connection error: %s"),sm_encode_html_special_chars($cpw_db->getMessage()))); |
| 156 | if ($cpw_peardb_debug) |
| 157 | array_push($msgs,sm_encode_html_special_chars($cpw_db->getuserinfo())); |
| 158 | return $msgs; |
| 159 | } |
| 160 | |
| 161 | // get table information |
| 162 | $table_info = $cpw_db->tableinfo($cpw_peardb_table); |
| 163 | if (PEAR::isError($table_info)) { |
| 164 | array_push($msgs,sprintf(_("Invalid table name: %s"),sm_encode_html_special_chars($cpw_peardb_table))); |
| 165 | $cpw_db->disconnect(); |
| 166 | return $msgs; |
| 167 | } |
| 168 | |
| 169 | if (empty($table_info)) { |
| 170 | array_push($msgs,_("User table is empty.")); |
| 171 | $cpw_db->disconnect(); |
| 172 | return $msgs; |
| 173 | } |
| 174 | |
| 175 | $cpw_peardb_uid_check=false; |
| 176 | $cpw_peardb_passwd_check=false; |
| 177 | $cpw_peardb_domain_check=(($cpw_peardb_domain_field=='')? true : false); |
| 178 | foreach($table_info as $key => $field_data) { |
| 179 | if ($field_data['name']==$cpw_peardb_uid_field) |
| 180 | $cpw_peardb_uid_check=true; |
| 181 | if ($field_data['name']==$cpw_peardb_passwd_field) |
| 182 | $cpw_peardb_passwd_check=true; |
| 183 | if ($cpw_peardb_domain_field!='' && $field_data['name']==$cpw_peardb_domain_field) |
| 184 | $cpw_peardb_domain_check=true; |
| 185 | } |
| 186 | if (! $cpw_peardb_uid_check) { |
| 187 | array_push($msgs,_("Invalid uid field.")); |
| 188 | } |
| 189 | if (! $cpw_peardb_passwd_check) { |
| 190 | array_push($msgs,_("Invalid password field")); |
| 191 | } |
| 192 | if (! $cpw_peardb_domain_check) { |
| 193 | array_push($msgs,_("Invalid domain field")); |
| 194 | } |
| 195 | if (! empty($msgs)) { |
| 196 | $cpw_db->disconnect(); |
| 197 | return $msgs; |
| 198 | } |
| 199 | |
| 200 | // find user's entry |
| 201 | $query='SELECT' |
| 202 | .' '.$cpw_db->quoteIdentifier($cpw_peardb_uid_field) |
| 203 | .', '.$cpw_db->quoteIdentifier($cpw_peardb_passwd_field) |
| 204 | .(($cpw_peardb_domain_field!='') ? ', '.$cpw_db->quoteIdentifier($cpw_peardb_domain_field):'') |
| 205 | .' FROM '.$cpw_db->quoteIdentifier($cpw_peardb_table) |
| 206 | .' WHERE ' |
| 207 | .$cpw_db->quoteIdentifier($cpw_peardb_uid_field).'='.$cpw_db->quoteSmart($user) |
| 208 | .(($cpw_peardb_domain_field!='') ? |
| 209 | ' AND '.$cpw_db->quoteIdentifier($cpw_peardb_domain_field).'='.$cpw_db->quoteSmart($user_domain): |
| 210 | ''); |
| 211 | $cpw_res=$cpw_db->query($query); |
| 212 | if (PEAR::isError($cpw_res)) { |
| 213 | array_push($msgs,sprintf(_("Query failed: %s"),sm_encode_html_special_chars($cpw_res->getMessage()))); |
| 214 | $cpw_db->disconnect(); |
| 215 | return $msgs; |
| 216 | } |
| 217 | |
| 218 | // make sure that there is only one user. |
| 219 | if ($cpw_res->numRows()==0) { |
| 220 | array_push($msgs,_("Unable to find user in user table.")); |
| 221 | $cpw_db->disconnect(); |
| 222 | return $msgs; |
| 223 | } |
| 224 | |
| 225 | if ($cpw_res->numRows()>1) { |
| 226 | array_push($msgs,_("Too many matches found in user table.")); |
| 227 | $cpw_db->disconnect(); |
| 228 | return $msgs; |
| 229 | } |
| 230 | |
| 231 | // FIXME: process possible errors |
| 232 | $cpw_res->fetchInto($userdb,DB_FETCHMODE_ASSOC); |
| 233 | |
| 234 | // validate password |
| 235 | $valid_passwd=false; |
| 236 | if ($cpw_peardb_crypted_passwd) { |
| 237 | // detect password type |
| 238 | $pw_type=cpw_peardb_detect_crypto($userdb[$cpw_peardb_passwd_field]); |
| 239 | if (! $pw_type) { |
| 240 | array_push($msgs,_("Unable to detect password crypto algorithm.")); |
| 241 | } else { |
| 242 | $hashed_pw=cpw_peardb_passwd_hash($curpw,$pw_type,$msgs,$userdb[$cpw_peardb_passwd_field]); |
| 243 | if ($hashed_pw==$userdb[$cpw_peardb_passwd_field]) { |
| 244 | $valid_passwd=true; |
| 245 | } |
| 246 | } |
| 247 | } elseif ($userdb[$cpw_peardb_passwd_field]==$curpw) { |
| 248 | $valid_passwd=true; |
| 249 | } |
| 250 | |
| 251 | if (! $valid_passwd) { |
| 252 | array_push($msgs,CPW_CURRENT_NOMATCH); |
| 253 | $cpw_db->disconnect(); |
| 254 | return $msgs; |
| 255 | } |
| 256 | |
| 257 | // create new password |
| 258 | if ($cpw_peardb_crypted_passwd) { |
| 259 | $hashed_passwd=cpw_peardb_passwd_hash($newpw,$pw_type,$msgs); |
| 260 | } else { |
| 261 | $hashed_passwd=$newpw; |
| 262 | } |
| 263 | |
| 264 | // make sure that password was created |
| 265 | if (! empty($msgs)) { |
| 266 | array_push($msgs,_("Unable to encrypt new password.")); |
| 267 | $cpw_db->disconnect(); |
| 268 | return $msgs; |
| 269 | } |
| 270 | |
| 271 | // create update query |
| 272 | $update_query='UPDATE ' |
| 273 | . $cpw_db->quoteIdentifier($cpw_peardb_table) |
| 274 | .' SET '.$cpw_db->quoteIdentifier($cpw_peardb_passwd_field) |
| 275 | .'='.$cpw_db->quoteSmart($hashed_passwd) |
| 276 | .' WHERE '.$cpw_db->quoteIdentifier($cpw_peardb_uid_field) |
| 277 | .'='.$cpw_db->quoteSmart($user) |
| 278 | .(($cpw_peardb_domain_field!='') ? |
| 279 | ' AND '.$cpw_db->quoteIdentifier($cpw_peardb_domain_field).'='.$cpw_db->quoteSmart($user_domain) : |
| 280 | ''); |
| 281 | |
| 282 | // update database |
| 283 | $cpw_res=$cpw_db->query($update_query); |
| 284 | |
| 285 | // check for update error |
| 286 | if (PEAR::isError($cpw_res)) { |
| 287 | array_push($msgs,sprintf(_("Unable to set new password: %s"),sm_encode_html_special_chars($cpw_res->getMessage()))); |
| 288 | } |
| 289 | |
| 290 | // close database connection |
| 291 | $cpw_db->disconnect(); |
| 292 | |
| 293 | return $msgs; |
| 294 | } |
| 295 | |
| 296 | /** |
| 297 | * Detects password crypto |
| 298 | * reports 'crypt' if fails to detect any other crypt |
| 299 | * @param string $password |
| 300 | * @return string |
| 301 | */ |
| 302 | function cpw_peardb_detect_crypto($password) { |
| 303 | $ret = false; |
| 304 | |
| 305 | if (preg_match("/^\{(.+)\}+/",$password,$crypto)) { |
| 306 | $ret=strtolower($crypto[1]); |
| 307 | } |
| 308 | |
| 309 | if ($ret=='crypt') { |
| 310 | // {CRYPT} can be standard des crypt, extended des crypt, md5 crypt or blowfish |
| 311 | // depends on first salt symbols (ext_des = '_', md5 = '$1$', blowfish = '$2') |
| 312 | // and length of salt (des = 2 chars, ext_des = 9, md5 = 12, blowfish = 16). |
| 313 | if (preg_match("/^\{crypt\}\\\$1\\\$+/i",$password)) { |
| 314 | $ret='tagged_md5crypt'; |
| 315 | } elseif (preg_match("/^\{crypt\}\\\$2+/i",$password)) { |
| 316 | $ret='tagged_blowfish'; |
| 317 | } elseif (preg_match("/^\{crypt\}_+/i",$password)) { |
| 318 | $ret='tagged_extcrypt'; |
| 319 | } else { |
| 320 | $ret='tagged_crypt'; |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | if (! $ret) { |
| 325 | if (preg_match("/^\\\$1\\\$+/i",$password)) { |
| 326 | $ret='md5crypt'; |
| 327 | } elseif (preg_match("/^\\\$2+/i",$password)) { |
| 328 | $ret='blowfish'; |
| 329 | } elseif (preg_match("/^_+/i",$password)) { |
| 330 | $ret='extcrypt'; |
| 331 | } else { |
| 332 | $ret='crypt'; |
| 333 | } |
| 334 | } |
| 335 | return $ret; |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * Encode password |
| 340 | * @param string $password plain text password |
| 341 | * @param string $crypto used crypto |
| 342 | * @param array $msgs error messages |
| 343 | * @param string $forced_salt old password used to create password hash for verification |
| 344 | * @return string hashed password. false, if hashing fails |
| 345 | */ |
| 346 | function cpw_peardb_passwd_hash($password,$crypto,&$msgs,$forced_salt='') { |
| 347 | global $username; |
| 348 | |
| 349 | $crypto = strtolower($crypto); |
| 350 | |
| 351 | $ret=false; |
| 352 | $salt=''; |
| 353 | // extra symbols used for random string in crypt salt |
| 354 | // squirrelmail GenerateRandomString() adds alphanumerics with third argument = 7. |
| 355 | $extra_salt_chars='./'; |
| 356 | |
| 357 | switch($crypto) { |
| 358 | case 'plain-md5': |
| 359 | $ret='{PLAIN-MD5}' . md5($password); |
| 360 | break; |
| 361 | case 'digest-md5': |
| 362 | // split username into user and domain parts |
| 363 | if (preg_match("/(.*)@(.*)/",$username,$match)) { |
| 364 | $ret='{DIGEST-MD5}' . md5($match[1].':'.$match[2].':'.$password); |
| 365 | } else { |
| 366 | array_push($msgs,_("Unable to use digest-md5 crypto.")); |
| 367 | } |
| 368 | break; |
| 369 | case 'tagged_crypt': |
| 370 | case 'crypt': |
| 371 | if (! defined('CRYPT_STD_DES') || CRYPT_STD_DES==0) { |
| 372 | array_push($msgs,sprintf(_("Unsupported crypto: %s"),'crypt')); |
| 373 | break; |
| 374 | } |
| 375 | if ($forced_salt=='') { |
| 376 | $salt=GenerateRandomString(2,$extra_salt_chars,7); |
| 377 | } else { |
| 378 | $salt=$forced_salt; |
| 379 | } |
| 380 | $ret = ($crypto=='tagged_crypt' ? '{crypt}' : ''); |
| 381 | $ret.= crypt($password,$salt); |
| 382 | break; |
| 383 | case 'tagged_md5crypt': |
| 384 | case 'md5crypt': |
| 385 | if (! defined('CRYPT_MD5') || CRYPT_MD5==0) { |
| 386 | array_push($msgs,sprintf(_("Unsupported crypto: %s"),'md5crypt')); |
| 387 | break; |
| 388 | } |
| 389 | if ($forced_salt=='') { |
| 390 | $salt='$1$' .GenerateRandomString(9,$extra_salt_chars,7); |
| 391 | } else { |
| 392 | $salt=$forced_salt; |
| 393 | } |
| 394 | $ret = ($crypto=='tagged_md5crypt' ? '{crypt}' : ''); |
| 395 | $ret.= crypt($password,$salt); |
| 396 | break; |
| 397 | case 'tagged_extcrypt': |
| 398 | case 'extcrypt': |
| 399 | if (! defined('CRYPT_EXT_DES') || CRYPT_EXT_DES==0) { |
| 400 | array_push($msgs,sprintf(_("Unsupported crypto: %s"),'extcrypt')); |
| 401 | break; |
| 402 | } |
| 403 | if ($forced_salt=='') { |
| 404 | $salt='_' . GenerateRandomString(8,$extra_salt_chars,7); |
| 405 | } else { |
| 406 | $salt=$forced_salt; |
| 407 | } |
| 408 | $ret = ($crypto=='tagged_extcrypt' ? '{crypt}' : ''); |
| 409 | $ret.= crypt($password,$salt); |
| 410 | break; |
| 411 | case 'tagged_blowfish': |
| 412 | case 'blowfish': |
| 413 | if (! defined('CRYPT_BLOWFISH') || CRYPT_BLOWFISH==0) { |
| 414 | array_push($msgs,sprintf(_("Unsupported crypto: %s"),'blowfish')); |
| 415 | break; |
| 416 | } |
| 417 | if ($forced_salt=='') { |
| 418 | $salt='$2a$12$' . GenerateRandomString(13,$extra_salt_chars,7); |
| 419 | } else { |
| 420 | $salt=$forced_salt; |
| 421 | } |
| 422 | $ret = ($crypto=='tagged_blowfish' ? '{crypt}' : ''); |
| 423 | $ret.= crypt($password,$salt); |
| 424 | break; |
| 425 | case 'plain': |
| 426 | case 'plaintext': |
| 427 | $ret = $password; |
| 428 | break; |
| 429 | default: |
| 430 | array_push($msgs,sprintf(_("Unsupported crypto: %s"),sm_encode_html_special_chars($crypto))); |
| 431 | } |
| 432 | return $ret; |
| 433 | } |