24
24
use Respect\Validation\Validator as v;
25
25
use Xibo\Exception\AccessDeniedException;
26
26
use Xibo\Exception\ConfigurationException;
27
use Xibo\Exception\DuplicateEntityException;
28
use Xibo\Exception\InvalidArgumentException;
29
27
use Xibo\Exception\LibraryFullException;
30
28
use Xibo\Exception\NotFoundException;
31
29
use Xibo\Factory\ApplicationScopeFactory;
39
37
use Xibo\Factory\UserFactory;
40
38
use Xibo\Factory\UserGroupFactory;
41
39
use Xibo\Factory\UserOptionFactory;
42
use Xibo\Helper\Pbkdf2Hash;
43
40
use Xibo\Service\ConfigServiceInterface;
44
41
use Xibo\Service\LogServiceInterface;
45
42
use Xibo\Storage\StorageServiceInterface;
44
// These constants may be changed without breaking existing hashes.
45
define("PBKDF2_HASH_ALGORITHM", "sha256");
46
define("PBKDF2_ITERATIONS", 1000);
47
define("PBKDF2_SALT_BYTES", 24);
48
define("PBKDF2_HASH_BYTES", 24);
50
define("HASH_SECTIONS", 4);
51
define("HASH_ALGORITHM_INDEX", 0);
52
define("HASH_ITERATION_INDEX", 1);
53
define("HASH_SALT_INDEX", 2);
54
define("HASH_PBKDF2_INDEX", 3);
49
58
* @package Xibo\Entity
459
468
$this->testPasswordAgainstPolicy($password);
461
$this->password = password_hash($password, PASSWORD_DEFAULT);
470
$this->password = $this->createHash($password);
475
* Is the user salted?
478
public function isSalted()
480
return ($this->CSPRNG == 1);
478
496
if ($this->password != md5($password))
479
497
throw new AccessDeniedException();
481
else if ($this->CSPRNG == 1) {
484
if (!Pbkdf2Hash::verifyPassword($password, $this->password)) {
485
$this->getLog()->debug('Password failed Pbkdf2Hash Check.');
486
throw new AccessDeniedException();
488
} catch (\InvalidArgumentException $e) {
489
$this->getLog()->warning('Invalid password hash stored for userId ' . $this->userId);
490
$this->getLog()->debug('Hash error: ' . $e->getMessage());
500
$params = explode(":", $this->password);
501
if (count($params) < HASH_SECTIONS) {
502
$this->getLog()->warning('Invalid password hash stored for userId %d', $this->userId);
503
throw new AccessDeniedException();
494
if (!password_verify($password, $this->password)) {
506
$pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]);
508
// Check to see if the hash created from the provided password is the same as the hash we have stored already
509
if (!$this->slowEquals($pbkdf2, $this->pbkdf2($params[HASH_ALGORITHM_INDEX], $password, $params[HASH_SALT_INDEX], (int)$params[HASH_ITERATION_INDEX], strlen($pbkdf2), true))) {
495
510
$this->getLog()->debug('Password failed Hash Check.');
496
511
throw new AccessDeniedException();
500
515
$this->getLog()->debug('Password checked out OK');
502
// Do we need to convert?
503
$this->updateHashIfRequired($password);
507
* Update hash if required
508
* @param string $password
510
private function updateHashIfRequired($password)
512
if (($this->CSPRNG == 0 || $this->CSPRNG == 1) || ($this->CSPRNG == 2 && password_needs_rehash($this->password, PASSWORD_DEFAULT))) {
513
$this->getLog()->debug('Converting password to use latest hash');
514
$this->setNewPassword($password);
515
$this->save(['validate' => false, 'passwordUpdate' => true]);
604
603
foreach ($this->events as $event) {
605
604
/* @var Schedule $event */
606
605
$event->setOwner($user->getOwnerId());
607
$event->setDisplayFactory($this->displayFactory);
608
$event->save(['generate' => false]);
606
$event->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory);
610
609
foreach ($this->layouts as $layout) {
611
610
/* @var Layout $layout */
638
630
public function validate()
640
if (!v::alnum('_')->length(1, 50)->validate($this->userName) && !v::email()->validate($this->userName))
641
throw new InvalidArgumentException(__('User name must be between 1 and 50 characters.'), 'userName');
632
if (!v::alnum('_')->length(1, 50)->validate($this->userName))
633
throw new \InvalidArgumentException(__('User name must be between 1 and 50 characters.'));
643
635
if (!v::string()->notEmpty()->validate($this->password))
644
throw new InvalidArgumentException(__('Please enter a Password.'), 'password');
636
throw new \InvalidArgumentException(__('Please enter a Password.'));
646
638
if (!v::int()->validate($this->libraryQuota))
647
throw new InvalidArgumentException(__('Library Quota must be a whole number.'), 'libraryQuota');
639
throw new \InvalidArgumentException(__('Library Quota must be a whole number.'));
649
641
if (!empty($this->email) && !v::email()->validate($this->email))
650
throw new InvalidArgumentException(__('Please enter a valid email address or leave it empty.'), 'email');
642
throw new \InvalidArgumentException(__('Please enter a valid email address or leave it empty.'));
653
645
$user = $this->userFactory->getByName($this->userName);
655
647
if ($this->userId == null || $this->userId != $user->userId)
656
throw new DuplicateEntityException(__('There is already a user with this name. Please choose another.'));
648
throw new \InvalidArgumentException(__('There is already a user with this name. Please choose another.'));
658
650
catch (NotFoundException $e) {
756
748
// Delete user specific entities
757
$this->getStore()->update('DELETE FROM `resolution` WHERE userId = :userId', ['userId' => $this->userId]);
758
749
$this->getStore()->update('DELETE FROM `session` WHERE userId = :userId', ['userId' => $this->userId]);
759
750
$this->getStore()->update('DELETE FROM `user` WHERE userId = :userId', ['userId' => $this->userId]);
985
976
$new->view = max($permission->view, $old->view);
986
977
$new->edit = max($permission->view, $old->view);
987
978
$new->delete = max($permission->view, $old->view);
989
$this->permissionCache[$entity][$permission->objectId] = $new;
992
981
$this->permissionCache[$entity][$permission->objectId] = $permission;
1003
992
private function checkObjectCompatibility($object)
1005
if (!method_exists($object, 'getId') || !method_exists($object, 'getOwnerId') || !method_exists($object, 'permissionsClass'))
994
if (!method_exists($object, 'getId') || !method_exists($object, 'getOwnerId'))
1006
995
throw new \InvalidArgumentException(__('Provided Object not under permission management'));
1027
1016
return $this->permissionFactory->getFullPermissions();
1029
1018
// Get the permissions for that entity
1030
$permissions = $this->loadPermissions($object->permissionsClass());
1019
$permissions = $this->loadPermissions(get_class($object));
1032
1021
// Check to see if our object is in the list
1033
1022
if (array_key_exists($object->getId(), $permissions))
1058
1047
// Get the permissions for that entity
1059
$permissions = $this->loadPermissions($object->permissionsClass());
1048
$permissions = $this->loadPermissions(get_class($object));
1061
1050
// Check to see if our object is in the list
1062
1051
if (array_key_exists($object->getId(), $permissions))
1116
1105
// Get the permissions for that entity
1117
$permissions = $this->loadPermissions($object->permissionsClass());
1106
$permissions = $this->loadPermissions(get_class($object));
1119
1108
// Check to see if our object is in the list
1120
1109
if (array_key_exists($object->getId(), $permissions))
1160
public function isSuperAdmin()
1162
return ($this->getUserTypeId() == 1);
1169
public function isGroupAdmin()
1171
return ($this->getUserTypeId() == 2);
1175
1146
* Is this users library quota full
1176
1147
* @throws LibraryFullException when the library is full or cannot be determined
1208
* Password hashing with PBKDF2.
1209
* Author: havoc AT defuse.ca
1210
* www: https://defuse.ca/php-pbkdf2.htm
1211
* @param string $password
1214
private function createHash($password)
1216
// format: algorithm:iterations:salt:hash
1217
$salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM));
1218
return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" . $salt . ":" .
1219
base64_encode($this->pbkdf2(
1220
PBKDF2_HASH_ALGORITHM,
1230
* Compares two strings $a and $b in length-constant time.
1235
private function slowEquals($a, $b)
1237
$diff = strlen($a) ^ strlen($b);
1238
for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
1240
$diff |= ord($a[$i]) ^ ord($b[$i]);
1237
1246
* Tests the supplied password against the password policy
1238
1247
* @param string $password
1251
1260
throw new \InvalidArgumentException($policyError);
1265
* PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
1267
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
1269
* This implementation of PBKDF2 was originally created by https://defuse.ca
1270
* With improvements by http://www.variations-of-shadow.com
1272
* @param string $algorithm The hash algorithm to use. Recommended: SHA256
1273
* @param string $password The password.
1274
* @param string $salt A salt that is unique to the password.
1275
* @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000.
1276
* @param int $key_length The length of the derived key in bytes.
1277
* @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise.
1278
* @return string A $key_length-byte key derived from the password and salt.
1280
public function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
1282
$algorithm = strtolower($algorithm);
1283
if (!in_array($algorithm, hash_algos(), true))
1284
throw new \InvalidArgumentException('PBKDF2 ERROR: Invalid hash algorithm.');
1285
if ($count <= 0 || $key_length <= 0)
1286
throw new \InvalidArgumentException('PBKDF2 ERROR: Invalid parameters.');
1288
$hash_length = strlen(hash($algorithm, "", true));
1289
$block_count = ceil($key_length / $hash_length);
1292
for ($i = 1; $i <= $block_count; $i++) {
1293
// $i encoded as 4 bytes, big endian.
1294
$last = $salt . pack("N", $i);
1296
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
1297
// perform the other $count - 1 iterations
1298
for ($j = 1; $j < $count; $j++) {
1299
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
1305
return substr($output, 0, $key_length);
1307
return bin2hex(substr($output, 0, $key_length));