2014-07-09 19:51:10 +02:00
< ? 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=1' ;
/**
* 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' ];
}
}
}
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!
}
$pw_enc = self :: CLEARTEXT ;
$data = array (
'acc_id' => $acc_id ,
'account_id' => $account_id ,
'cred_username' => $username ,
'cred_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 )))
{
2014-08-29 18:37:30 +02:00
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] ! " );
2014-07-09 19:51:10 +02:00
}
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 ) 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' );
2014-09-02 12:45:54 +02:00
if ( empty ( $session_key ))
{
error_log ( __METHOD__ . " () no session password available! " );
return false ;
}
2014-07-09 19:51:10 +02:00
$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 ();