Completed
Push — 14.2 ( c37920...a817b0 )
by Ralf
51:41 queued 23:45
created

emailadmin_credentials::delete()   D

Complexity

Conditions 9
Paths 25

Size

Total Lines 28
Code Lines 15

Duplication

Lines 4
Ratio 14.29 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
eloc 15
c 1
b 1
f 0
nc 25
nop 4
dl 4
loc 28
rs 4.909
1
<?php
2
/**
3
 * EGroupware EMailAdmin: Mail account credentials
4
 *
5
 * @link http://www.stylite.de
6
 * @package emailadmin
7
 * @author Ralf Becker <[email protected]>
8
 * @copyright (c) 2013-14 by Ralf Becker <[email protected]>
9
 * @author Stylite AG <[email protected]>
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
/**
15
 * Mail account credentials are stored in egw_ea_credentials for given
16
 * acocunt-id, users and types (imap, smtp and optional admin connection).
17
 *
18
 * Passwords in credentials are encrypted with either user password from session
19
 * or the database password.
20
 */
21
class emailadmin_credentials
22
{
23
	const APP = 'emailadmin';
24
	const TABLE = 'egw_ea_credentials';
25
	const USER_EDITABLE_JOIN = 'JOIN egw_ea_accounts ON egw_ea_accounts.acc_id=egw_ea_credentials.acc_id AND acc_user_editable=';
26
27
	/**
28
	 * Credentials for type IMAP
29
	 */
30
	const IMAP = 1;
31
	/**
32
	 * Credentials for type SMTP
33
	 */
34
	const SMTP = 2;
35
	/**
36
	 * Credentials for admin connection
37
	 */
38
	const ADMIN = 8;
39
	/**
40
	 * All credentials IMAP|SMTP|ADMIN
41
	 */
42
	const ALL = 11;
43
44
	/**
45
	 * Password in cleartext
46
	 */
47
	const CLEARTEXT = 0;
48
	/**
49
	 * Password encrypted with user password
50
	 */
51
	const USER = 1;
52
	/**
53
	 * Password encrypted with system secret
54
	 */
55
	const SYSTEM = 2;
56
57
	/**
58
	 * Returned for passwords, when an admin reads an accounts with a password encrypted with users session password
59
	 */
60
	const UNAVAILABLE = '**unavailable**';
61
62
	/**
63
	 * Translate type to prefix
64
	 *
65
	 * @var array
66
	 */
67
	protected static $type2prefix = array(
68
		self::IMAP => 'acc_imap_',
69
		self::SMTP => 'acc_smtp_',
70
		self::ADMIN => 'acc_imap_admin_',
71
	);
72
73
	/**
74
	 * Reference to global db object
75
	 *
76
	 * @var egw_db
77
	 */
78
	static protected $db;
79
80
	/**
81
	 * Mcrypt instance initialised with system specific key
82
	 *
83
	 * @var ressource
84
	 */
85
	static protected $system_mcrypt;
86
87
	/**
88
	 * Mcrypt instance initialised with user password from session
89
	 *
90
	 * @var ressource
91
	 */
92
	static protected $user_mcrypt;
93
94
	/**
95
	 * Cache for credentials to minimize database access
96
	 *
97
	 * @var array
98
	 */
99
	protected static $cache = array();
100
101
	/**
102
	 * Read credentials for a given mail account
103
	 *
104
	 * @param int $acc_id
105
	 * @param int $type =null default return all credentials
106
	 * @param int|array $account_id =null default use current user or all (in that order)
107
	 * @return array with values for (imap|smtp|admin)_(username|password|cred_id)
108
	 */
109
	public static function read($acc_id, $type=null, $account_id=null)
110
	{
111
		if (is_null($type)) $type = self::ALL;
112 View Code Duplication
		if (is_null($account_id))
113
		{
114
			$account_id = array(0, $GLOBALS['egw_info']['user']['account_id']);
115
		}
116
117
		// check cache, if nothing found, query database
118
		// check assumes always same accounts (eg. 0=all plus own account_id) are asked
119
		if (!isset(self::$cache[$acc_id]) ||
120
			!($rows = array_intersect_key(self::$cache[$acc_id], array_flip((array)$account_id))))
121
		{
122
			$rows = self::$db->select(self::TABLE, '*', array(
123
				'acc_id' => $acc_id,
124
				'account_id' => $account_id,
125
				'(cred_type & '.(int)$type.') > 0',	// postgreSQL require > 0, or gives error as it expects boolean
126
			), __LINE__, __FILE__, false,
127
				// account_id DESC ensures 0=all allways overwrite (old user-specific credentials)
128
				'ORDER BY account_id ASC', self::APP);
129
			//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") nothing in cache");
130
		}
131
		else
132
		{
133
			ksort($rows);	// ORDER BY account_id ASC
134
135
			// flatten account_id => cred_type => row array again, to have format like from database
136
			$rows = call_user_func_array('array_merge', $rows);
137
			//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") read from cache ".array2string($rows));
138
		}
139
		$results = array();
140
		foreach($rows as $row)
141
		{
142
			// update cache (only if we have database-iterator and all credentials asked!)
143
			if (!is_array($rows) && $type == self::ALL)
144
			{
145
				self::$cache[$acc_id][$row['account_id']][$row['cred_type']] = $row;
146
				//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") stored to cache ".array2string($row));
147
			}
148
			$password = self::decrypt($row);
149
150
			foreach(self::$type2prefix as $pattern => $prefix)
151
			{
152
				if ($row['cred_type'] & $pattern)
153
				{
154
					$results[$prefix.'username'] = $row['cred_username'];
155
					$results[$prefix.'password'] = $password;
156
					$results[$prefix.'cred_id'] = $row['cred_id'];
157
					$results[$prefix.'account_id'] = $row['account_id'];
158
					$results[$prefix.'pw_enc'] = $row['cred_pw_enc'];
159
				}
160
			}
161
		}
162
		return $results;
163
	}
164
165
	/**
166
	 * Generate username according to acc_imap_logintype and fetch password from session
167
	 *
168
	 * @param array $data values for acc_imap_logintype and acc_domain
169
	 * @param boolean $set_identity =true true: also set identity values realname&email, if not yet set
170
	 * @return array with values for keys 'acc_(imap|smtp)_(username|password|cred_id)'
171
	 */
172
	public static function from_session(array $data, $set_identity=true)
173
	{
174
		switch($data['acc_imap_logintype'])
175
		{
176
			case 'standard':
177
				$username = $GLOBALS['egw_info']['user']['account_lid'];
178
				break;
179
180
			case 'vmailmgr':
181
				$username = $GLOBALS['egw_info']['user']['account_lid'].'@'.$data['acc_domain'];
182
				break;
183
184
			case 'email':
185
				$username = $GLOBALS['egw_info']['user']['account_email'];
186
				break;
187
188
			case 'uidNumber':
189
				$username = 'u'.$GLOBALS['egw_info']['user']['account_id'].'@'.$data['acc_domain'];
190
				break;
191
192
			case 'admin':
193
				// data should have been stored in credentials table
194
				throw new egw_exception_assertion_failed('data[acc_imap_logintype]=admin and no stored username/password for data[acc_id]='.$data['acc_id'].'!');
195
196
			default:
197
				throw new egw_exception_wrong_parameter("Unknown data[acc_imap_logintype]=".array2string($data['acc_imap_logintype']).'!');
198
		}
199
		$password = base64_decode(egw_cache::getSession('phpgwapi', 'password'));
200
		$realname = !$set_identity || $data['ident_realname'] ? $data['ident_realname'] :
201
			$GLOBALS['egw_info']['user']['account_fullname'];
202
		$email = !$set_identity || $data['ident_email'] ? $data['ident_email'] :
203
			$GLOBALS['egw_info']['user']['account_email'];
204
205
		return array(
206
			'ident_realname' => $realname,
207
			'ident_email' => $email,
208
			'acc_imap_username' => $username,
209
			'acc_imap_password' => $password,
210
			'acc_imap_cred_id'  => $data['acc_imap_logintype'],	// to NOT store it
211
			'acc_imap_account_id' => 'c',
212
		) + ($data['acc_smtp_auth_session'] ? array(
213
			// only set smtp
214
			'acc_smtp_username' => $username,
215
			'acc_smtp_password' => $password,
216
			'acc_smtp_cred_id'  => $data['acc_imap_logintype'],	// to NOT store it
217
			'acc_smtp_account_id' => 'c',
218
		) : array());
219
	}
220
221
	/**
222
	 * Write and encrypt credentials
223
	 *
224
	 * @param int $acc_id id of account
225
	 * @param string $username
226
	 * @param string $password cleartext password to write
227
	 * @param int $type self::IMAP, self::SMTP or self::ADMIN
228
	 * @param int $account_id if of user-account for whom credentials are
229
	 * @param int $cred_id =null id of existing credentials to update
230
	 * @param ressource $mcrypt =null mcrypt ressource for user, default calling self::init_crypt(true)
231
	 * @return int cred_id
232
	 */
233
	public static function write($acc_id, $username, $password, $type, $account_id=0, $cred_id=null, $mcrypt=null)
234
	{
235
		//error_log(__METHOD__."(acc_id=$acc_id, '$username', \$password, type=$type, account_id=$account_id, cred_id=$cred_id)");
236
		if (!empty($cred_id) && !is_numeric($cred_id) || !is_numeric($account_id))
237
		{
238
			//error_log(__METHOD__."($acc_id, '$username', \$password, $type, $account_id, ".array2string($cred_id).") not storing session credentials!");
239
			return;	// do NOT store credentials from session of current user!
240
		}
241
		// no need to write empty usernames, but delete existing row
242
		if ((string)$username === '')
243
		{
244
			if ($cred_id) self::$db->delete(self::TABLE, array('cred_id' => $cred_id), __LINE__, __FILE__, self::APP);
245
			return;	// nothing to save
246
		}
247
		$pw_enc = self::CLEARTEXT;
248
		$data = array(
249
			'acc_id' => $acc_id,
250
			'account_id' => $account_id,
251
			'cred_username' => $username,
252
			'cred_password' => (string)$password === '' ? '' :
253
				self::encrypt($password, $account_id, $pw_enc, $mcrypt),
254
			'cred_type' => $type,
255
			'cred_pw_enc' => $pw_enc,
256
		);
257
		// check if password is unavailable (admin edits an account with password encrypted with users session PW) and NOT store it
258
		if ($password == self::UNAVAILABLE)
259
		{
260
			//error_log(__METHOD__."(".array2string(func_get_args()).") can NOT store unavailable password, storing without password!");
261
			unset($data['cred_password'], $data['cred_pw_enc']);
262
		}
263
		//error_log(__METHOD__."($acc_id, '$username', '$password', $type, $account_id, $cred_id, $mcrypt) storing ".array2string($data).' '.function_backtrace());
264
		if ($cred_id > 0)
265
		{
266
			self::$db->update(self::TABLE, $data, array('cred_id' => $cred_id), __LINE__, __FILE__, self::APP);
267
		}
268
		else
269
		{
270
			self::$db->insert(self::TABLE, $data, array(
271
				'acc_id' => $acc_id,
272
				'account_id' => $account_id,
273
				'cred_type' => $type,
274
			), __LINE__, __FILE__, self::APP);
275
			$cred_id = self::$db->get_last_insert_id(self::TABLE, 'cred_id');
276
		}
277
		// invalidate cache
278
		unset(self::$cache[$acc_id][$account_id]);
279
280
		//error_log(__METHOD__."($acc_id, '$username', \$password, $type, $account_id) returning $cred_id");
281
		return $cred_id;
282
	}
283
284
	/**
285
	 * Delete credentials from database
286
	 *
287
	 * @param int $acc_id
288
	 * @param int|array $account_id =null
289
	 * @param int $type =self::ALL self::IMAP, self::SMTP or self::ADMIN
290
	 * @param boolean $exact_type =false true: delete only cred_type=$type, false: delete cred_type&$type
291
	 * @return int number of rows deleted
292
	 */
293
	public static function delete($acc_id, $account_id=null, $type=self::ALL, $exact_type=false)
294
	{
295
		if (!($acc_id > 0) && !isset($account_id))
296
		{
297
			throw new egw_exception_wrong_parameter(__METHOD__."() no acc_id AND no account_id parameter!");
298
		}
299
		$where = array();
300
		if ($acc_id > 0) $where['acc_id'] = $acc_id;
301
		if (isset($account_id)) $where['account_id'] = $account_id;
302
		if ($exact_type)
303
		{
304
			$where['cred_type'] = $type;
305
		}
306
		elseif ($type != self::ALL)
307
		{
308
			$where[] = '(cred_type & '.(int)$type.') > 0';	// postgreSQL require > 0, or gives error as it expects boolean
309
		}
310
		self::$db->delete(self::TABLE, $where, __LINE__, __FILE__, self::APP);
311
312
		// invalidate cache: we allways unset everything about an account to simplify cache handling
313 View Code Duplication
		foreach($acc_id > 0 ? (array)$acc_id : array_keys(self::$cache) as $acc_id)
314
		{
315
			unset(self::$cache[$acc_id]);
316
		}
317
		$ret = self::$db->affected_rows();
318
		//error_log(__METHOD__."($acc_id, ".array2string($account_id).", $type) affected $ret rows");
319
		return $ret;
320
	}
321
322
	/**
323
	 * Encrypt password for storing in database
324
	 *
325
	 * @param string $password cleartext password
326
	 * @param int $account_id user-account password is for
327
	 * @param int &$pw_enc on return encryption used
328
	 * @param ressource $mcrypt =null mcrypt ressource for user, default calling self::init_crypt(true)
329
	 * @return string encrypted password
330
	 */
331
	protected static function encrypt($password, $account_id, &$pw_enc, $mcrypt=null)
332
	{
333
		if ($account_id > 0 && $account_id == $GLOBALS['egw_info']['user']['account_id'] &&
334
			($mcrypt || ($mcrypt = self::init_crypt(true))))
335
		{
336
			$pw_enc = self::USER;
337
			$password = mcrypt_generic($mcrypt, $password);
338
		}
339
		elseif (($mcrypt = self::init_crypt(false)))
340
		{
341
			$pw_enc = self::SYSTEM;
342
			$password = mcrypt_generic($mcrypt, $password);
343
		}
344
		else
345
		{
346
			$pw_enc = self::CLEARTEXT;
347
		}
348
		//error_log(__METHOD__."(, $account_id, , $mcrypt) pw_enc=$pw_enc returning ".array2string(base64_encode($password)));
349
		return base64_encode($password);
350
	}
351
352
	/**
353
	 * Decrypt password from database
354
	 *
355
	 * @param array $row database row
356
	 * @param ressource $mcrypt =null mcrypt ressource for user, default calling self::init_crypt(true)
357
	 */
358
	protected static function decrypt(array $row, $mcrypt=null)
359
	{
360
		switch ($row['cred_pw_enc'])
361
		{
362
			case self::CLEARTEXT:
363
				return base64_decode($row['cred_password']);
364
365
			case self::USER:
366
				if ($row['account_id'] != $GLOBALS['egw_info']['user']['account_id'])
367
				{
368
					return self::UNAVAILABLE;
369
				}
370
				// fall through
371
			case self::SYSTEM:
372
				if (($row['cred_pw_enc'] != self::USER || !$mcrypt) &&
373
					!($mcrypt = self::init_crypt($row['cred_pw_enc'] == self::USER)))
374
				{
375
					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]!");
376
				}
377
				return !empty($row['cred_password']) ? trim(mdecrypt_generic($mcrypt, base64_decode($row['cred_password']))) : '';
378
		}
379
		throw new egw_exception_wrong_parameter("Unknow password encryption type $row[cred_pw_enc]!");
380
	}
381
382
	/**
383
	 * Hook called when user changes his password, to re-encode his credentials with his new password
384
	 *
385
	 * It also changes all user credentials encoded with system password!
386
	 *
387
	 * It only changes credentials from user-editable accounts, as user probably
388
	 * does NOT know password set by admin!
389
	 *
390
	 * @param array $data values for keys 'old_passwd', 'new_passwd', 'account_id'
391
	 */
392
	static public function changepassword(array $data)
393
	{
394
		if (empty($data['old_passwd'])) return;
395
396
		$old_mcrypt = null;
397
		foreach(self::$db->select(self::TABLE, self::TABLE.'.*', array(
398
			'account_id' => $data['account_id']
399
		),__LINE__, __FILE__, false, '', 'emailadmin', 0, self::USER_EDITABLE_JOIN.self::$db->quote(true, 'bool')) as $row)
400
		{
401
			if (!isset($old_mcrypt))
402
			{
403
				$old_mcrypt = self::init_crypt($data['old_passwd']);
404
				$new_mcrypt = self::init_crypt($data['new_passwd']);
405
				if (!$old_mcrypt && !$new_mcrypt) return;
406
			}
407
			$password = self::decrypt($row, $old_mcrypt);
0 ignored issues
show
Security Bug introduced by
It seems like $old_mcrypt defined by self::init_crypt($data['old_passwd']) on line 403 can also be of type false; however, emailadmin_credentials::decrypt() does only seem to accept object<ressource>|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
408
409
			self::write($row['acc_id'], $row['cred_username'], $password, $row['cred_type'],
410
				$row['account_id'], $row['cred_id'], $new_mcrypt);
0 ignored issues
show
Security Bug introduced by
It seems like $new_mcrypt defined by self::init_crypt($data['new_passwd']) on line 404 can also be of type false; however, emailadmin_credentials::write() does only seem to accept object<ressource>|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
411
		}
412
	}
413
414
	/**
415
	 * Check if session encryption is configured, possible and initialise it
416
	 *
417
	 * @param boolean|string $user =false true: use user-password from session,
418
	 *	false: database password or string with password to use
419
	 * @param string $algo ='tripledes'
420
	 * @param string $mode ='ecb'
421
	 * @return ressource|boolean mcrypt ressource to use or false if not available
422
	 */
423
	static public function init_crypt($user=false, $algo='tripledes',$mode='ecb')
424
	{
425
		if (is_string($user))
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
426
		{
427
			// do NOT use/set/change static object
428
		}
429
		elseif ($user)
430
		{
431
			$mcrypt =& self::$user_mcrypt;
432
		}
433
		else
434
		{
435
			$mcrypt =& self::$system_mcrypt;
436
		}
437
		if (!isset($mcrypt))
438
		{
439
			if (is_string($user))
440
			{
441
				$key = $user;
442
			}
443
			elseif ($user)
444
			{
445
				$session_key = egw_cache::getSession('phpgwapi', 'password');
446
				if (empty($session_key))
447
				{
448
					error_log(__METHOD__."() no session password available!");
449
					return false;
450
				}
451
				$key = base64_decode($session_key);
452
			}
453
			else
454
			{
455
				$key = self::$db->Password;
456
			}
457
			if (!check_load_extension('mcrypt'))
458
			{
459
				error_log(__METHOD__."() required PHP extension mcrypt not loaded and can not be loaded, passwords can be NOT encrypted!");
460
				$mcrypt = false;
461
			}
462
			elseif (!($mcrypt = mcrypt_module_open($algo, '', $mode, '')))
463
			{
464
				error_log(__METHOD__."() could not mcrypt_module_open(algo='$algo','',mode='$mode',''), passwords can be NOT encrypted!");
465
				$mcrypt = false;
466
			}
467
			else
468
			{
469
				$iv_size = mcrypt_enc_get_iv_size($mcrypt);
470
				$iv = !isset($GLOBALS['egw_info']['server']['mcrypt_iv']) || strlen($GLOBALS['egw_info']['server']['mcrypt_iv']) < $iv_size ?
471
					mcrypt_create_iv ($iv_size, MCRYPT_RAND) : substr($GLOBALS['egw_info']['server']['mcrypt_iv'],0,$iv_size);
472
473
				$key_size = mcrypt_enc_get_key_size($mcrypt);
474
				if (bytes($key) > $key_size) $key = cut_bytes($key,0,$key_size-1);
475
476
				if (mcrypt_generic_init($mcrypt, $key, $iv) < 0)
477
				{
478
					error_log(__METHOD__."() could not initialise mcrypt, passwords can be NOT encrypted!");
479
					$mcrypt = false;
480
				}
481
			}
482
		}
483
		//error_log(__METHOD__."(".array2string($user).") key=".array2string($key)." returning ".array2string($mcrypt));
484
		return $mcrypt;
485
	}
486
487
	/**
488
	 * Init our static properties
489
	 */
490
	static public function init_static()
491
	{
492
		self::$db = isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
493
	}
494
}
495
emailadmin_credentials::init_static();
496