2016-03-28 20:51:38 +02:00
< ? php
/**
* EGroupware Api : Mail account credentials
*
* @ link http :// www . stylite . de
* @ package api
* @ subpackage mail
* @ author Ralf Becker < rb - AT - stylite . de >
* @ copyright ( c ) 2013 - 16 by Ralf Becker < rb - AT - stylite . de >
* @ author Stylite AG < info @ stylite . de >
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ version $Id $
*/
namespace EGroupware\Api\Mail ;
use EGroupware\Api ;
/**
* Mail account credentials are stored in egw_ea_credentials for given
2016-07-11 21:38:36 +02:00
* account_id , users and types ( imap , smtp and optional admin connection ) .
2016-03-28 20:51:38 +02:00
*
* Passwords in credentials are encrypted with either user password from session
* or the database password .
2016-06-19 14:49:50 +02:00
*
* If OpenSSL extension is available it is used to store credentials with AES - 128 - CBC ,
2016-07-11 21:38:36 +02:00
* with key generated via hash_pbkdf2 sha256 hash and 16 byte binary salt ( = 24 char base64 ) .
2016-06-19 14:49:50 +02:00
* OpenSSL can be also used to read old MCrypt credentials ( OpenSSL 'des-ede3' ) .
*
* If only MCrypt is available ( or EGroupware versions 14. x ) credentials are are stored
* with MCrypt algo 'tripledes' and mode 'ecb' . Key is direct user password or system secret ,
* key - size 24 ( truncated to 23 byte , if greater then 24 byte ! This is a bug , but thats how it is stored . ) .
2016-03-28 20:51:38 +02:00
*/
class Credentials
{
2016-04-06 10:48:52 +02:00
/**
* App tables belong to
*/
const APP = 'api' ;
/**
* Name of credentials table
*/
2016-03-28 20:51:38 +02:00
const TABLE = 'egw_ea_credentials' ;
2016-04-06 10:48:52 +02:00
/**
* Join to check account is user - editable
*/
2016-03-28 20:51:38 +02:00
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 ;
/**
2017-01-25 18:03:35 +01:00
* Credentials for SMIME private key
2017-01-25 11:39:31 +01:00
*/
const SMIME = 16 ;
/**
2019-06-05 13:10:25 +02:00
* Two factor auth secret key
2016-03-28 20:51:38 +02:00
*/
2019-06-05 13:10:25 +02:00
const TWOFA = 32 ;
2019-07-29 17:26:49 +02:00
/**
* Collabora key
2019-07-29 17:36:27 +02:00
*
* @ link https :// github . com / EGroupware / collabora / blob / master / src / Credentials . php #L20
2019-07-29 17:26:49 +02:00
*/
const COLLABORA = 64 ;
2019-07-29 17:36:27 +02:00
2021-02-08 16:31:22 +01:00
/**
* SpamTitan API Token
*/
const SPAMTITAN = 128 ;
2019-06-05 13:10:25 +02:00
/**
* All credentials
*/
2021-02-08 16:31:22 +01:00
const ALL = self :: IMAP | self :: SMTP | self :: ADMIN | self :: SMIME | self :: TWOFA | self :: SPAMTITAN ;
2016-03-28 20:51:38 +02:00
/**
* Password in cleartext
*/
const CLEARTEXT = 0 ;
/**
* Password encrypted with user password
2016-06-19 14:49:50 +02:00
*
* MCrypt algo 'tripledes' and mode 'ecb' or OpenSSL 'des-ede3'
* Key is direct user password , key - size 24 ( truncated to 23 byte , if greater then 24 byte ! )
2016-03-28 20:51:38 +02:00
*/
const USER = 1 ;
/**
* Password encrypted with system secret
2016-06-19 14:49:50 +02:00
*
* MCrypt algo 'tripledes' and mode 'ecb' or OpenSSL 'des-ede3'
* Key is direct system secret , key - size 24 ( truncated to 23 byte , if greater then 24 byte ! )
2016-03-28 20:51:38 +02:00
*/
const SYSTEM = 2 ;
2016-06-19 14:49:50 +02:00
/**
* Password encrypted with user password
*
* OpenSSL : AES - 128 - CBC , with key generated via hash_pbkdf2 sha256 hash and 12 byte binary salt ( = 16 char base64 )
*/
const USER_AES = 3 ;
/**
* Password encrypted with system secret
*
* OpenSSL : AES - 128 - CBC , with key generated via hash_pbkdf2 sha256 hash and 12 byte binary salt ( = 16 char base64 )
*/
const SYSTEM_AES = 4 ;
2016-03-28 20:51:38 +02:00
2016-06-01 16:24:47 +02:00
/**
* Returned for passwords , when an admin reads an accounts with a password encrypted with users session password
*/
const UNAVAILABLE = '**unavailable**' ;
2016-03-28 20:51:38 +02:00
/**
* Translate type to prefix
*
* @ var array
*/
protected static $type2prefix = array (
self :: IMAP => 'acc_imap_' ,
self :: SMTP => 'acc_smtp_' ,
self :: ADMIN => 'acc_imap_admin_' ,
2017-01-25 11:39:31 +01:00
self :: SMIME => 'acc_smime_' ,
2019-06-05 13:10:25 +02:00
self :: TWOFA => '2fa_' ,
2021-02-08 16:31:22 +01:00
self :: SPAMTITAN => 'acc_spam_' ,
2016-03-28 20:51:38 +02:00
);
/**
* Mcrypt instance initialised with system specific key
*
2021-03-31 17:49:43 +02:00
* @ var resource
2016-03-28 20:51:38 +02:00
*/
static protected $system_mcrypt ;
/**
* Mcrypt instance initialised with user password from session
*
2021-03-31 17:49:43 +02:00
* @ var resource
2016-03-28 20:51:38 +02:00
*/
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 )
2016-07-07 16:08:08 +02:00
* @ param array & $on_login = null on return array with callable and further arguments
* to run on successful login to trigger password migration
2016-03-28 20:51:38 +02:00
* @ return array with values for ( imap | smtp | admin ) _ ( username | password | cred_id )
*/
2016-07-07 16:08:08 +02:00
public static function read ( $acc_id , $type = null , $account_id = null , & $on_login = null )
2016-03-28 20:51:38 +02:00
{
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 ))))
{
2018-08-29 18:04:07 +02:00
$rows = self :: get_db () -> select ( self :: TABLE , '*' , array (
2016-03-28 20:51:38 +02:00
'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));
}
2016-07-07 16:08:08 +02:00
$on_login = null ;
2016-03-28 20:51:38 +02:00
$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));
2016-07-07 16:08:08 +02:00
if ( ! isset ( $on_login ) && self :: needMigration ( $row [ 'cred_pw_enc' ]))
{
$on_login = array ( __CLASS__ . '::migrate' , $acc_id );
}
2016-03-28 20:51:38 +02:00
}
$password = self :: decrypt ( $row );
2017-08-31 11:39:50 +02:00
// Remove special x char added to the end for \0 trimming escape.
if ( $type == self :: SMIME && substr ( $password , - 1 ) === 'x' ) $password = substr ( $password , 0 , - 1 );
2019-01-31 22:14:03 +01:00
foreach ( static :: $type2prefix as $pattern => $prefix )
2016-03-28 20:51:38 +02:00
{
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 Api\Exception\AssertionFailed ( 'data[acc_imap_logintype]=admin and no stored username/password for data[acc_id]=' . $data [ 'acc_id' ] . '!' );
default :
throw new Api\Exception\WrongParameter ( " Unknown data[acc_imap_logintype]= " . array2string ( $data [ 'acc_imap_logintype' ]) . '!' );
}
$password = base64_decode ( Api\Cache :: getSession ( 'phpgwapi' , 'password' ));
2021-10-08 15:43:48 +02:00
$realname = ! $set_identity || ! empty ( $data [ 'ident_realname' ]) ? $data [ 'ident_realname' ] :
( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_fullname' ] ? ? null );
$email = ! $set_identity || ! empty ( $data [ 'ident_email' ]) ? $data [ 'ident_email' ] :
( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_email' ] ? ? null );
2016-03-28 20:51:38 +02:00
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
2017-01-25 18:03:35 +01:00
* @ param int $type self :: IMAP , self :: SMTP , self :: ADMIN or self :: SMIME
2016-03-28 20:51:38 +02:00
* @ param int $account_id if of user - account for whom credentials are
* @ param int $cred_id = null id of existing credentials to update
* @ return int cred_id
*/
2016-06-19 14:49:50 +02:00
public static function write ( $acc_id , $username , $password , $type , $account_id = 0 , $cred_id = null )
2016-03-28 20:51:38 +02:00
{
//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!
}
2018-08-29 18:04:07 +02:00
2017-08-31 11:39:50 +02:00
// Add arbitary char to the ending to make sure the Smime binary content
// with \0 at the end not getting trimmed of while trying to decrypt.
if ( $type == self :: SMIME ) $password .= 'x' ;
2016-03-28 20:51:38 +02:00
// no need to write empty usernames, but delete existing row
if (( string ) $username === '' )
{
2018-08-29 18:04:07 +02:00
if ( $cred_id ) self :: get_db () -> delete ( self :: TABLE , array ( 'cred_id' => $cred_id ), __LINE__ , __FILE__ , self :: APP );
2016-03-28 20:51:38 +02:00
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 === '' ? '' :
2016-06-19 14:49:50 +02:00
self :: encrypt ( $password , $account_id , $pw_enc ),
2016-03-28 20:51:38 +02:00
'cred_type' => $type ,
'cred_pw_enc' => $pw_enc ,
);
2016-06-01 16:24:47 +02:00
// check if password is unavailable (admin edits an account with password encrypted with users session PW) and NOT store it
if ( $password == self :: UNAVAILABLE )
{
2016-06-01 16:41:30 +02:00
//error_log(__METHOD__."(".array2string(func_get_args()).") can NOT store unavailable password, storing without password!");
2016-06-01 16:24:47 +02:00
unset ( $data [ 'cred_password' ], $data [ 'cred_pw_enc' ]);
}
2016-06-19 14:49:50 +02:00
//error_log(__METHOD__."($acc_id, '$username', '$password', $type, $account_id, $cred_id) storing ".array2string($data).' '.function_backtrace());
2016-03-28 20:51:38 +02:00
if ( $cred_id > 0 )
{
2018-08-29 18:04:07 +02:00
self :: get_db () -> update ( self :: TABLE , $data , array ( 'cred_id' => $cred_id ), __LINE__ , __FILE__ , self :: APP );
2016-03-28 20:51:38 +02:00
}
else
{
2018-08-29 18:04:07 +02:00
self :: get_db () -> insert ( self :: TABLE , $data , array (
2016-03-28 20:51:38 +02:00
'acc_id' => $acc_id ,
'account_id' => $account_id ,
'cred_type' => $type ,
), __LINE__ , __FILE__ , self :: APP );
2018-08-29 18:04:07 +02:00
$cred_id = self :: get_db () -> get_last_insert_id ( self :: TABLE , 'cred_id' );
2016-03-28 20:51:38 +02:00
}
// 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
2017-01-25 18:03:35 +01:00
* @ param int $type = self :: IMAP , self :: SMTP , self :: ADMIN or self :: SMIME
2016-06-01 16:24:47 +02:00
* @ param boolean $exact_type = false true : delete only cred_type = $type , false : delete cred_type & $type
2016-03-28 20:51:38 +02:00
* @ return int number of rows deleted
*/
2016-06-01 16:24:47 +02:00
public static function delete ( $acc_id , $account_id = null , $type = self :: ALL , $exact_type = false )
2016-03-28 20:51:38 +02:00
{
if ( ! ( $acc_id > 0 ) && ! isset ( $account_id ))
{
throw new Api\Exception\WrongParameter ( __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 ;
2016-06-01 16:24:47 +02:00
if ( $exact_type )
{
$where [ 'cred_type' ] = $type ;
}
elseif ( $type != self :: ALL )
{
$where [] = '(cred_type & ' . ( int ) $type . ') > 0' ; // postgreSQL require > 0, or gives error as it expects boolean
}
2018-08-29 18:04:07 +02:00
self :: get_db () -> delete ( self :: TABLE , $where , __LINE__ , __FILE__ , self :: APP );
2016-03-28 20:51:38 +02:00
// 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 ]);
}
2018-08-29 18:04:07 +02:00
$ret = self :: get_db () -> affected_rows ();
2016-03-28 20:51:38 +02:00
//error_log(__METHOD__."($acc_id, ".array2string($account_id).", $type) affected $ret rows");
return $ret ;
}
/**
2016-06-19 14:49:50 +02:00
* Encrypt password for storing in database with MCrypt and tripledes mode cbc
*
* @ param string $password cleartext password
* @ param int $account_id user - account password is for
2016-08-09 11:48:56 +02:00
* @ param int & $pw_enc on return encryption used
2016-06-19 14:49:50 +02:00
* @ return string encrypted password
*/
2020-06-12 18:56:44 +02:00
public static function encrypt ( $password , $account_id , & $pw_enc )
2016-06-19 14:49:50 +02:00
{
try {
return self :: encrypt_openssl_aes ( $password , $account_id , $pw_enc );
}
catch ( Api\Exception\AssertionFailed $ex ) {
try {
return self :: encrypt_mcrypt_3des ( $password , $account_id , $pw_enc );
}
catch ( Api\Exception\AssertionFailed $ex ) {
$pw_enc = self :: CLEARTEXT ;
return base64_encode ( $password );
}
}
}
/**
* OpenSSL method to use for AES encrypted credentials
*/
const AES_METHOD = 'AES-128-CBC' ;
/**
* Len ( binary ) of salt / iv used for pbkdf2 and openssl
*/
const SALT_LEN = 16 ;
/**
* Len of base64 encoded salt prefixing AES encoded credentials ( 4 * ceil ( SALT_LEN / 3 ))
*/
const SALT_LEN64 = 24 ;
/**
* Encrypt password for storing in database via OpenSSL and AES
*
* @ param string $password cleartext password
* @ param int $account_id user - account password is for
2016-08-09 11:48:56 +02:00
* @ param int & $pw_enc on return encryption used
2016-06-19 14:49:50 +02:00
* @ param string $key = null key / password to use , default password according to account_id
2016-07-11 21:38:36 +02:00
* @ param string $salt = null ( binary ) salt to use , default generate new random salt
2016-06-19 14:49:50 +02:00
* @ return string encrypted password
*/
2016-07-11 21:38:36 +02:00
protected static function encrypt_openssl_aes ( $password , $account_id , & $pw_enc , $key = null , $salt = null )
2016-06-19 14:49:50 +02:00
{
if ( empty ( $key ))
{
if ( $account_id > 0 && $account_id == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] &&
( $key = Api\Cache :: getSession ( 'phpgwapi' , 'password' )))
{
$pw_enc = self :: USER_AES ;
$key = base64_decode ( $key );
}
else
{
$pw_enc = self :: SYSTEM_AES ;
2018-08-29 18:04:07 +02:00
$key = self :: get_db () -> Password ;
2016-06-19 14:49:50 +02:00
}
}
// using a pbkdf2 password derivation with a (stored) salt
$aes_key = self :: aes_key ( $key , $salt );
return base64_encode ( $salt ) . base64_encode ( openssl_encrypt ( $password , self :: AES_METHOD , $aes_key , OPENSSL_RAW_DATA , $salt ));
}
/**
* Derive an encryption key from a password
*
* Using a pbkdf2 password derivation with a ( stored ) salt
* With a 12 byte binary ( 16 byte base64 ) salt we can store 39 byte password in our varchar ( 80 ) column .
*
* @ param string $password
* @ param string & $salt binary salt to use or null to generate one , on return used salt
* @ param int $iterations = 2048 iterations of passsword
* @ param int $length = 16 length of binary aes key
* @ param string $hash = 'sha256'
* @ return string
*/
protected static function aes_key ( $password , & $salt , $iterations = 2048 , $length = 16 , $hash = 'sha256' )
{
if ( empty ( $salt ))
{
$salt = openssl_random_pseudo_bytes ( self :: SALT_LEN );
}
// load hash_pbkdf2 polyfill for php < 5.5
if ( ! function_exists ( 'hash_pbkdf2' ))
{
require_once __DIR__ . '/hash_pbkdf2.php' ;
}
$aes_key = hash_pbkdf2 ( $hash , $password , $salt , $iterations , $length , true );
//error_log(__METHOD__."('$password', '".base64_encode($salt)."') returning ".base64_encode($aes_key).' '.function_backtrace());
return $aes_key ;
}
/**
* Encrypt password for storing in database with MCrypt and tripledes mode cbc
2016-03-28 20:51:38 +02:00
*
* @ param string $password cleartext password
* @ param int $account_id user - account password is for
2016-08-09 11:48:56 +02:00
* @ param int & $pw_enc on return encryption used
2016-03-28 20:51:38 +02:00
* @ return string encrypted password
*/
2016-06-19 14:49:50 +02:00
protected static function encrypt_mcrypt_3des ( $password , $account_id , & $pw_enc )
2016-03-28 20:51:38 +02:00
{
if ( $account_id > 0 && $account_id == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] &&
2016-06-19 14:49:50 +02:00
( $mcrypt = self :: init_crypt ( true )))
2016-03-28 20:51:38 +02:00
{
$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
2016-08-09 11:48:56 +02:00
* @ param string $key = null key / password to use , default user pw from session or database pw , see get_key
2016-06-19 14:49:50 +02:00
* @ return string cleartext password
* @ throws Api\Exception\WrongParameter
* @ throws Api\Exception\AssertionFailed if neither OpenSSL nor MCrypt extension available
2016-03-28 20:51:38 +02:00
*/
2020-06-12 18:56:44 +02:00
public static function decrypt ( array $row , $key = null )
2016-03-28 20:51:38 +02:00
{
2016-06-19 14:49:50 +02:00
// empty/unset passwords only give warnings ...
if ( empty ( $row [ 'cred_password' ])) return '' ;
if ( self :: isUser ( $row [ 'cred_pw_enc' ]) && $row [ 'account_id' ] != $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ])
{
return self :: UNAVAILABLE ;
}
switch ( $row [ 'cred_pw_enc' ])
2016-03-28 20:51:38 +02:00
{
case self :: CLEARTEXT :
return base64_decode ( $row [ 'cred_password' ]);
2016-06-19 14:49:50 +02:00
case self :: USER_AES :
case self :: SYSTEM_AES :
2016-08-09 11:48:56 +02:00
return self :: decrypt_openssl_aes ( $row , $key );
2016-06-19 14:49:50 +02:00
2016-03-28 20:51:38 +02:00
case self :: USER :
case self :: SYSTEM :
2016-06-19 14:49:50 +02:00
try {
2016-08-09 11:48:56 +02:00
$password = self :: decrypt_openssl_3des ( $row , $key );
2016-06-19 14:49:50 +02:00
// ToDo store as AES
return $password ;
2016-03-28 20:51:38 +02:00
}
2016-06-19 14:49:50 +02:00
catch ( Api\Exception\AssertionFailed $e ) {
unset ( $e );
// try Mcrypt
return self :: decrypt_mcrypt_3des ( $row );
}
}
throw new Api\Exception\WrongParameter ( " Password encryption type $row[cred_pw_enc] NOT available for mail account # $row[acc_id] and user # $row[account_id] / $row[cred_username] ! " );
}
/**
* Decrypt tripledes password from database with Mcrypt
*
* @ param array $row database row
* @ return string cleartext password
2016-08-09 11:48:56 +02:00
* @ param string $key = null key / password to use , default user pw from session or database pw , see get_key
2016-06-19 14:49:50 +02:00
* @ throws Api\Exception\WrongParameter
* @ throws Api\Exception\AssertionFailed if MCrypt extension not available
*/
2016-08-09 11:48:56 +02:00
protected static function decrypt_mcrypt_3des ( array $row , $key = null )
2016-06-19 14:49:50 +02:00
{
check_load_extension ( 'mcrypt' , true );
2016-08-09 11:48:56 +02:00
if ( ! ( $mcrypt = self :: init_crypt ( isset ( $key ) ? $key : $row [ 'cred_pw_enc' ] == self :: USER )))
2016-06-19 14:49:50 +02:00
{
throw new Api\Exception\WrongParameter ( " Password encryption type $row[cred_pw_enc] NOT available for mail account # $row[acc_id] and user # $row[account_id] / $row[cred_username] ! " );
}
return trim ( mdecrypt_generic ( $mcrypt , base64_decode ( $row [ 'cred_password' ])), " \0 " );
}
/**
* Get key / password to decrypt credentials
*
* @ param int $pw_enc self :: ( SYSTEM | USER )( _AES ) ?
* @ return string
* @ throws Api\Exception\AssertionFailed if not session password is available
*/
protected static function get_key ( $pw_enc )
{
if ( self :: isUser ( $pw_enc ))
{
$session_key = Api\Cache :: getSession ( 'phpgwapi' , 'password' );
if ( empty ( $session_key ))
{
throw new Api\Exception\AssertionFailed ( " No session password available! " );
}
$key = base64_decode ( $session_key );
}
else
{
2018-08-29 18:04:07 +02:00
$key = self :: get_db () -> Password ;
2016-03-28 20:51:38 +02:00
}
2016-06-19 14:49:50 +02:00
return $key ;
}
/**
* OpenSSL equivalent for Mcrypt $algo = 'tripledes' , $mode = 'ecb'
*/
const TRIPLEDES_ECB_METHOD = 'des-ede3' ;
/**
* Decrypt tripledes password from database with OpenSSL
*
* Seems iv is NOT used for mcrypt " tripledes/ecb " = openssl " des-ede3 " , only key - size 24.
*
* @ link https :// github . com / tom --/ mcrypt2openssl / blob / master / mapping . md
* @ link http :// thefsb . tumblr . com / post / 110749271235 / using - opensslendecrypt - in - php - instead - of
* @ param array $row database row
* @ param string $key = null password to use
* @ return string cleartext password
* @ throws Api\Exception\WrongParameter
* @ throws Api\Exception\AssertionFailed if OpenSSL extension not available
*/
protected static function decrypt_openssl_3des ( array $row , $key = null )
{
check_load_extension ( 'openssl' , true );
if ( ! isset ( $key ) || ! is_string ( $key ))
{
$key = self :: get_key ( $row [ 'cred_pw_enc' ]);
}
// seems iv is NOT used for mcrypt "tripledes/ecb" = openssl "des-ede3", only key-size 24
$keySize = 24 ;
if ( bytes ( $key ) > $keySize ) $key = cut_bytes ( $key , 0 , $keySize - 1 ); // $keySize-1 is wrong, but that's what's used!
return trim ( openssl_decrypt ( $row [ 'cred_password' ], self :: TRIPLEDES_ECB_METHOD , $key , OPENSSL_ZERO_PADDING , '' ), " \0 " );
}
/**
* Decrypt aes encrypted and salted password from database via OpenSSL and AES
*
* @ param array $row database row
* @ param string $key = null password to use
* @ param string $salt_len = 16 len of base64 encoded salt ( binary is 3 / 4 )
* @ return string cleartext password
* @ throws Api\Exception\WrongParameter
* @ throws Api\Exception\AssertionFailed if OpenSSL extension not available
*/
protected static function decrypt_openssl_aes ( array $row , $key = null )
{
check_load_extension ( 'openssl' , true );
if ( ! isset ( $key ) || ! is_string ( $key ))
{
$key = self :: get_key ( $row [ 'cred_pw_enc' ]);
}
$salt = base64_decode ( substr ( $row [ 'cred_password' ], 0 , self :: SALT_LEN64 ));
$aes_key = self :: aes_key ( $key , $salt );
return trim ( openssl_decrypt ( base64_decode ( substr ( $row [ 'cred_password' ], self :: SALT_LEN64 )),
self :: AES_METHOD , $aes_key , OPENSSL_RAW_DATA , $salt ), " \0 " );
2016-03-28 20:51:38 +02:00
}
2016-07-07 16:08:08 +02:00
/**
* Check if credentials need migration to AES
*
* @ param string $pw_enc
* @ return boolean
*/
static public function needMigration ( $pw_enc )
{
return $pw_enc == self :: USER || $pw_enc == self :: SYSTEM || $pw_enc == self :: CLEARTEXT ;
}
/**
* Run password migration for credentials of given account
*
* @ param int $acc_id
*/
static function migrate ( $acc_id )
{
try {
2016-07-07 16:23:30 +02:00
if ( isset ( self :: $cache [ $acc_id ]))
2016-07-07 16:08:08 +02:00
{
2016-07-07 16:23:30 +02:00
foreach ( self :: $cache [ $acc_id ] as $account_id => & $rows )
2016-07-07 16:08:08 +02:00
{
2016-07-07 16:23:30 +02:00
foreach ( $rows as $cred_type => & $row )
2016-07-07 16:08:08 +02:00
{
2016-07-07 16:23:30 +02:00
if ( self :: needMigration ( $row [ 'cred_pw_enc' ]) && ( $row [ 'cred_pw_enc' ] != self :: USER ||
$row [ 'cred_pw_enc' ] == self :: USER && $account_id == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]))
{
self :: write ( $acc_id , $row [ 'cred_username' ], self :: decrypt ( $row ), $cred_type , $account_id , $row [ 'cred_id' ]);
}
2016-07-07 16:08:08 +02:00
}
}
}
}
2016-07-07 16:36:06 +02:00
catch ( \Exception $e ) {
2016-07-07 16:08:08 +02:00
// do not stall regular use, if password migration fails
_egw_log_exception ( $e );
}
}
2016-03-28 20:51:38 +02:00
/**
* 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 ;
2016-08-09 11:48:56 +02:00
// as self::encrypt will use password in session, check it is identical to given new password
if ( $data [ 'new_passwd' ] !== base64_decode ( Api\Cache :: getSession ( 'phpgwapi' , 'password' )))
{
throw new Api\Exception\AssertionFailed ( 'Password in session !== password given in $data[new_password]!' );
}
2018-08-29 18:04:07 +02:00
foreach ( self :: get_db () -> select ( self :: TABLE , self :: TABLE . '.*' , array (
2016-03-28 20:51:38 +02:00
'account_id' => $data [ 'account_id' ]
2019-10-15 18:33:35 +02:00
), __LINE__ , __FILE__ , false , '' , self :: APP ) as $row )
2016-03-28 20:51:38 +02:00
{
2016-08-09 11:48:56 +02:00
$password = self :: decrypt ( $row , self :: isUser ( $row [ 'cred_pw_enc' ]) ? $data [ 'old_passwd' ] : null );
2016-03-28 20:51:38 +02:00
self :: write ( $row [ 'acc_id' ], $row [ 'cred_username' ], $password , $row [ 'cred_type' ],
2016-08-09 11:48:56 +02:00
$row [ 'account_id' ], $row [ 'cred_id' ]);
2016-03-28 20:51:38 +02:00
}
}
/**
* 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 = Api\Cache :: getSession ( 'phpgwapi' , 'password' );
if ( empty ( $session_key ))
{
error_log ( __METHOD__ . " () no session password available! " );
return false ;
}
$key = base64_decode ( $session_key );
}
else
{
2018-08-29 18:04:07 +02:00
$key = self :: get_db () -> Password ;
2016-03-28 20:51:38 +02:00
}
2016-06-19 14:49:50 +02:00
check_load_extension ( 'mcrypt' , true );
if ( ! ( $mcrypt = mcrypt_module_open ( $algo , '' , $mode , '' )))
2016-03-28 20:51:38 +02:00
{
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 ?
2021-03-31 17:49:43 +02:00
mcrypt_create_iv ( $iv_size , MCRYPT_DEV_RANDOM ) : substr ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'mcrypt_iv' ], 0 , $iv_size );
2016-03-28 20:51:38 +02:00
$key_size = mcrypt_enc_get_key_size ( $mcrypt );
if ( bytes ( $key ) > $key_size ) $key = cut_bytes ( $key , 0 , $key_size - 1 );
2021-03-31 17:49:43 +02:00
if ( ! $iv || mcrypt_generic_init ( $mcrypt , $key , $iv ) < 0 )
2016-03-28 20:51:38 +02:00
{
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 ;
}
2016-06-19 14:49:50 +02:00
/**
* Check if credentials are encrypted with users session password
*
* @ param string $pw_enc
* @ return boolean
*/
static public function isUser ( $pw_enc )
{
return $pw_enc == self :: USER_AES || $pw_enc == self :: USER ;
}
2016-03-28 20:51:38 +02:00
/**
2018-08-29 18:04:07 +02:00
* Get the current Db object , from either setup or egw
2019-01-31 22:14:03 +01:00
*
2019-08-01 18:38:07 +02:00
* @ return Api\Db
2016-03-28 20:51:38 +02:00
*/
2018-08-29 18:04:07 +02:00
static public function get_db ()
2016-03-28 20:51:38 +02:00
{
2018-08-29 18:04:07 +02:00
return isset ( $GLOBALS [ 'egw_setup' ]) ? $GLOBALS [ 'egw_setup' ] -> db : $GLOBALS [ 'egw' ] -> db ;
2016-03-28 20:51:38 +02:00
}
}