egroupware/emailadmin/inc/class.emailadmin_credentials.inc.php

473 lines
15 KiB
PHP

<?php
/**
* EGroupware EMailAdmin: Mail account credentials
*
* @link http://www.stylite.de
* @package emailadmin
* @author Ralf Becker <rb@stylite.de>
* @copyright (c) 2013-14 by Ralf Becker <rb@stylite.de>
* @author Stylite AG <info@stylite.de>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
/**
* Mail account credentials are stored in egw_ea_credentials for given
* acocunt-id, users and types (imap, smtp and optional admin connection).
*
* Passwords in credentials are encrypted with either user password from session
* or the database password.
*/
class emailadmin_credentials
{
const APP = 'emailadmin';
const TABLE = 'egw_ea_credentials';
const USER_EDITABLE_JOIN = 'JOIN egw_ea_accounts ON egw_ea_accounts.acc_id=egw_ea_credentials.acc_id AND acc_user_editable=';
/**
* Credentials for type IMAP
*/
const IMAP = 1;
/**
* Credentials for type SMTP
*/
const SMTP = 2;
/**
* Credentials for admin connection
*/
const ADMIN = 8;
/**
* All credentials IMAP|SMTP|ADMIN
*/
const ALL = 11;
/**
* Password in cleartext
*/
const CLEARTEXT = 0;
/**
* Password encrypted with user password
*/
const USER = 1;
/**
* Password encrypted with system secret
*/
const SYSTEM = 2;
/**
* Translate type to prefix
*
* @var array
*/
protected static $type2prefix = array(
self::IMAP => 'acc_imap_',
self::SMTP => 'acc_smtp_',
self::ADMIN => 'acc_imap_admin_',
);
/**
* Reference to global db object
*
* @var egw_db
*/
static protected $db;
/**
* Mcrypt instance initialised with system specific key
*
* @var ressource
*/
static protected $system_mcrypt;
/**
* Mcrypt instance initialised with user password from session
*
* @var ressource
*/
static protected $user_mcrypt;
/**
* Cache for credentials to minimize database access
*
* @var array
*/
protected static $cache = array();
/**
* Read credentials for a given mail account
*
* @param int $acc_id
* @param int $type =null default return all credentials
* @param int|array $account_id =null default use current user or all (in that order)
* @return array with values for (imap|smtp|admin)_(username|password|cred_id)
*/
public static function read($acc_id, $type=null, $account_id=null)
{
if (is_null($type)) $type = self::ALL;
if (is_null($account_id))
{
$account_id = array(0, $GLOBALS['egw_info']['user']['account_id']);
}
// check cache, if nothing found, query database
// check assumes always same accounts (eg. 0=all plus own account_id) are asked
if (!isset(self::$cache[$acc_id]) ||
!($rows = array_intersect_key(self::$cache[$acc_id], array_flip((array)$account_id))))
{
$rows = self::$db->select(self::TABLE, '*', array(
'acc_id' => $acc_id,
'account_id' => $account_id,
'(cred_type & '.(int)$type.') > 0', // postgreSQL require > 0, or gives error as it expects boolean
), __LINE__, __FILE__, false,
// account_id DESC ensures 0=all allways overwrite (old user-specific credentials)
'ORDER BY account_id ASC', self::APP);
//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") nothing in cache");
}
else
{
ksort($rows); // ORDER BY account_id ASC
// flatten account_id => cred_type => row array again, to have format like from database
$rows = call_user_func_array('array_merge', $rows);
//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") read from cache ".array2string($rows));
}
$results = array();
foreach($rows as $row)
{
// update cache (only if we have database-iterator and all credentials asked!)
if (!is_array($rows) && $type == self::ALL)
{
self::$cache[$acc_id][$row['account_id']][$row['cred_type']] = $row;
//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") stored to cache ".array2string($row));
}
$password = self::decrypt($row);
foreach(self::$type2prefix as $pattern => $prefix)
{
if ($row['cred_type'] & $pattern)
{
$results[$prefix.'username'] = $row['cred_username'];
$results[$prefix.'password'] = $password;
$results[$prefix.'cred_id'] = $row['cred_id'];
$results[$prefix.'account_id'] = $row['account_id'];
$results[$prefix.'pw_enc'] = $row['cred_pw_enc'];
}
}
}
return $results;
}
/**
* Generate username according to acc_imap_logintype and fetch password from session
*
* @param array $data values for acc_imap_logintype and acc_domain
* @param boolean $set_identity =true true: also set identity values realname&email, if not yet set
* @return array with values for keys 'acc_(imap|smtp)_(username|password|cred_id)'
*/
public static function from_session(array $data, $set_identity=true)
{
switch($data['acc_imap_logintype'])
{
case 'standard':
$username = $GLOBALS['egw_info']['user']['account_lid'];
break;
case 'vmailmgr':
$username = $GLOBALS['egw_info']['user']['account_lid'].'@'.$data['acc_domain'];
break;
case 'email':
$username = $GLOBALS['egw_info']['user']['account_email'];
break;
case 'uidNumber':
$username = 'u'.$GLOBALS['egw_info']['user']['account_id'].'@'.$data['acc_domain'];
break;
case 'admin':
// data should have been stored in credentials table
throw new egw_exception_assertion_failed('data[acc_imap_logintype]=admin and no stored username/password for data[acc_id]='.$data['acc_id'].'!');
default:
throw new egw_exception_wrong_parameter("Unknown data[acc_imap_logintype]=".array2string($data['acc_imap_logintype']).'!');
}
$password = base64_decode(egw_cache::getSession('phpgwapi', 'password'));
$realname = !$set_identity || $data['ident_realname'] ? $data['ident_realname'] :
$GLOBALS['egw_info']['user']['account_fullname'];
$email = !$set_identity || $data['ident_email'] ? $data['ident_email'] :
$GLOBALS['egw_info']['user']['account_email'];
return array(
'ident_realname' => $realname,
'ident_email' => $email,
'acc_imap_username' => $username,
'acc_imap_password' => $password,
'acc_imap_cred_id' => $data['acc_imap_logintype'], // to NOT store it
'acc_imap_account_id' => 'c',
) + ($data['acc_smtp_auth_session'] ? array(
// only set smtp
'acc_smtp_username' => $username,
'acc_smtp_password' => $password,
'acc_smtp_cred_id' => $data['acc_imap_logintype'], // to NOT store it
'acc_smtp_account_id' => 'c',
) : array());
}
/**
* Write and encrypt credentials
*
* @param int $acc_id id of account
* @param string $username
* @param string $password cleartext password to write
* @param int $type self::IMAP, self::SMTP or self::ADMIN
* @param int $account_id if of user-account for whom credentials are
* @param int $cred_id =null id of existing credentials to update
* @param ressource $mcrypt =null mcrypt ressource for user, default calling self::init_crypt(true)
* @return int cred_id
*/
public static function write($acc_id, $username, $password, $type, $account_id=0, $cred_id=null, $mcrypt=null)
{
//error_log(__METHOD__."(acc_id=$acc_id, '$username', \$password, type=$type, account_id=$account_id, cred_id=$cred_id)");
if (!empty($cred_id) && !is_numeric($cred_id) || !is_numeric($account_id))
{
//error_log(__METHOD__."($acc_id, '$username', \$password, $type, $account_id, ".array2string($cred_id).") not storing session credentials!");
return; // do NOT store credentials from session of current user!
}
// no need to write empty usernames, but delete existing row
if ((string)$username === '')
{
if ($cred_id) self::$db->delete(self::TABLE, array('cred_id' => $cred_id), __LINE__, __FILE__, self::APP);
return; // nothing to save
}
$pw_enc = self::CLEARTEXT;
$data = array(
'acc_id' => $acc_id,
'account_id' => $account_id,
'cred_username' => $username,
'cred_password' => (string)$password === '' ? '' :
self::encrypt($password, $account_id, $pw_enc, $mcrypt),
'cred_type' => $type,
'cred_pw_enc' => $pw_enc,
);
//error_log(__METHOD__."($acc_id, '$username', '$password', $type, $account_id, $cred_id, $mcrypt) storing ".array2string($data).' '.function_backtrace());
if ($cred_id > 0)
{
self::$db->update(self::TABLE, $data, array('cred_id' => $cred_id), __LINE__, __FILE__, self::APP);
}
else
{
self::$db->insert(self::TABLE, $data, array(
'acc_id' => $acc_id,
'account_id' => $account_id,
'cred_type' => $type,
), __LINE__, __FILE__, self::APP);
$cred_id = self::$db->get_last_insert_id(self::TABLE, 'cred_id');
}
// invalidate cache
unset(self::$cache[$acc_id][$account_id]);
//error_log(__METHOD__."($acc_id, '$username', \$password, $type, $account_id) returning $cred_id");
return $cred_id;
}
/**
* Delete credentials from database
*
* @param int $acc_id
* @param int|array $account_id =null
* @param int $type =self::ALL self::IMAP, self::SMTP or self::ADMIN
* @return int number of rows deleted
*/
public static function delete($acc_id, $account_id=null, $type=self::ALL)
{
if (!($acc_id > 0) && !isset($account_id))
{
throw new egw_exception_wrong_parameter(__METHOD__."() no acc_id AND no account_id parameter!");
}
$where = array();
if ($acc_id > 0) $where['acc_id'] = $acc_id;
if (isset($account_id)) $where['account_id'] = $account_id;
if ($type != self::ALL) $where[] = '(cred_type & '.(int)$type.') > 0'; // postgreSQL require > 0, or gives error as it expects boolean
self::$db->delete(self::TABLE, $where, __LINE__, __FILE__, self::APP);
// invalidate cache: we allways unset everything about an account to simplify cache handling
foreach($acc_id > 0 ? (array)$acc_id : array_keys(self::$cache) as $acc_id)
{
unset(self::$cache[$acc_id]);
}
$ret = self::$db->affected_rows();
//error_log(__METHOD__."($acc_id, ".array2string($account_id).", $type) affected $ret rows");
return $ret;
}
/**
* Encrypt password for storing in database
*
* @param string $password cleartext password
* @param int $account_id user-account password is for
* @param int &$pw_enc on return encryption used
* @param ressource $mcrypt =null mcrypt ressource for user, default calling self::init_crypt(true)
* @return string encrypted password
*/
protected static function encrypt($password, $account_id, &$pw_enc, $mcrypt=null)
{
if ($account_id > 0 && $account_id == $GLOBALS['egw_info']['user']['account_id'] &&
($mcrypt || ($mcrypt = self::init_crypt(true))))
{
$pw_enc = self::USER;
$password = mcrypt_generic($mcrypt, $password);
}
elseif (($mcrypt = self::init_crypt(false)))
{
$pw_enc = self::SYSTEM;
$password = mcrypt_generic($mcrypt, $password);
}
else
{
$pw_enc = self::CLEARTEXT;
}
//error_log(__METHOD__."(, $account_id, , $mcrypt) pw_enc=$pw_enc returning ".array2string(base64_encode($password)));
return base64_encode($password);
}
/**
* Decrypt password from database
*
* @param array $row database row
* @param ressource $mcrypt =null mcrypt ressource for user, default calling self::init_crypt(true)
*/
protected static function decrypt(array $row, $mcrypt=null)
{
switch ($row['cred_pw_enc'])
{
case self::CLEARTEXT:
return base64_decode($row['cred_password']);
case self::USER:
case self::SYSTEM:
if (($row['cred_pw_enc'] != self::USER || !$mcrypt) &&
!($mcrypt = self::init_crypt($row['cred_pw_enc'] == self::USER)))
{
throw new egw_exception_wrong_parameter("Password encryption type $row[cred_pw_enc] NOT available for mail account #$row[acc_id] and user #$row[account_id]/$row[cred_username]!");
}
return !empty($row['cred_password']) ? trim(mdecrypt_generic($mcrypt, base64_decode($row['cred_password']))) : '';
}
throw new egw_exception_wrong_parameter("Unknow password encryption type $row[cred_pw_enc]!");
}
/**
* Hook called when user changes his password, to re-encode his credentials with his new password
*
* It also changes all user credentials encoded with system password!
*
* It only changes credentials from user-editable accounts, as user probably
* does NOT know password set by admin!
*
* @param array $data values for keys 'old_passwd', 'new_passwd', 'account_id'
*/
static public function changepassword(array $data)
{
if (empty($data['old_passwd'])) return;
$old_mcrypt = null;
foreach(self::$db->select(self::TABLE, self::TABLE.'.*', array(
'account_id' => $data['account_id']
),__LINE__, __FILE__, false, '', 'emailadmin', 0, self::USER_EDITABLE_JOIN.self::$db->quote(true, 'bool')) as $row)
{
if (!isset($old_mcrypt))
{
$old_mcrypt = self::init_crypt($data['old_passwd']);
$new_mcrypt = self::init_crypt($data['new_passwd']);
if (!$old_mcrypt && !$new_mcrypt) return;
}
$password = self::decrypt($row, $old_mcrypt);
self::write($row['acc_id'], $row['cred_username'], $password, $row['cred_type'],
$row['account_id'], $row['cred_id'], $new_mcrypt);
}
}
/**
* Check if session encryption is configured, possible and initialise it
*
* @param boolean|string $user =false true: use user-password from session,
* false: database password or string with password to use
* @param string $algo ='tripledes'
* @param string $mode ='ecb'
* @return ressource|boolean mcrypt ressource to use or false if not available
*/
static public function init_crypt($user=false, $algo='tripledes',$mode='ecb')
{
if (is_string($user))
{
// do NOT use/set/change static object
}
elseif ($user)
{
$mcrypt =& self::$user_mcrypt;
}
else
{
$mcrypt =& self::$system_mcrypt;
}
if (!isset($mcrypt))
{
if (is_string($user))
{
$key = $user;
}
elseif ($user)
{
$session_key = egw_cache::getSession('phpgwapi', 'password');
if (empty($session_key))
{
error_log(__METHOD__."() no session password available!");
return false;
}
$key = base64_decode($session_key);
}
else
{
$key = self::$db->Password;
}
if (!check_load_extension('mcrypt'))
{
error_log(__METHOD__."() required PHP extension mcrypt not loaded and can not be loaded, passwords can be NOT encrypted!");
$mcrypt = false;
}
elseif (!($mcrypt = mcrypt_module_open($algo, '', $mode, '')))
{
error_log(__METHOD__."() could not mcrypt_module_open(algo='$algo','',mode='$mode',''), passwords can be NOT encrypted!");
$mcrypt = false;
}
else
{
$iv_size = mcrypt_enc_get_iv_size($mcrypt);
$iv = !isset($GLOBALS['egw_info']['server']['mcrypt_iv']) || strlen($GLOBALS['egw_info']['server']['mcrypt_iv']) < $iv_size ?
mcrypt_create_iv ($iv_size, MCRYPT_RAND) : substr($GLOBALS['egw_info']['server']['mcrypt_iv'],0,$iv_size);
$key_size = mcrypt_enc_get_key_size($mcrypt);
if (bytes($key) > $key_size) $key = cut_bytes($key,0,$key_size-1);
if (mcrypt_generic_init($mcrypt, $key, $iv) < 0)
{
error_log(__METHOD__."() could not initialise mcrypt, passwords can be NOT encrypted!");
$mcrypt = false;
}
}
}
//error_log(__METHOD__."(".array2string($user).") key=".array2string($key)." returning ".array2string($mcrypt));
return $mcrypt;
}
/**
* Init our static properties
*/
static public function init_static()
{
self::$db = isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
}
}
emailadmin_credentials::init_static();