~xibo-maintainers/xibo/tempel

« back to all changes in this revision

Viewing changes to lib/Entity/User.php

  • Committer: Dan Garner
  • Date: 2016-06-28 15:02:11 UTC
  • mto: This revision was merged to the branch mainline in revision 528.
  • Revision ID: git-v1:51031805c36c1d366fa330b2c2320d1927c57003
Fixes for upgrade steps

Show diffs side-by-side

added added

removed removed

Lines of Context:
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;
46
43
 
 
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);
 
49
 
 
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);
 
55
 
47
56
/**
48
57
 * Class User
49
58
 * @package Xibo\Entity
458
467
 
459
468
        $this->testPasswordAgainstPolicy($password);
460
469
 
461
 
        $this->password = password_hash($password, PASSWORD_DEFAULT);
462
 
        $this->CSPRNG = 2;
 
470
        $this->password = $this->createHash($password);
 
471
        $this->CSPRNG = 1;
 
472
    }
 
473
 
 
474
    /**
 
475
     * Is the user salted?
 
476
     * @return bool
 
477
     */
 
478
    public function isSalted()
 
479
    {
 
480
        return ($this->CSPRNG == 1);
463
481
    }
464
482
 
465
483
    /**
478
496
            if ($this->password != md5($password))
479
497
                throw new AccessDeniedException();
480
498
        }
481
 
        else if ($this->CSPRNG == 1) {
482
 
            // Test with Pbkdf2
483
 
            try {
484
 
                if (!Pbkdf2Hash::verifyPassword($password, $this->password)) {
485
 
                    $this->getLog()->debug('Password failed Pbkdf2Hash Check.');
486
 
                    throw new AccessDeniedException();
487
 
                }
488
 
            } catch (\InvalidArgumentException $e) {
489
 
                $this->getLog()->warning('Invalid password hash stored for userId ' . $this->userId);
490
 
                $this->getLog()->debug('Hash error: ' . $e->getMessage());
 
499
        else {
 
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();
491
504
            }
492
 
        }
493
 
        else {
494
 
            if (!password_verify($password, $this->password)) {
 
505
 
 
506
            $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]);
 
507
 
 
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();
497
512
            }
498
513
        }
499
514
 
500
515
        $this->getLog()->debug('Password checked out OK');
501
 
 
502
 
        // Do we need to convert?
503
 
        $this->updateHashIfRequired($password);
504
 
    }
505
 
 
506
 
    /**
507
 
     * Update hash if required
508
 
     * @param string $password
509
 
     */
510
 
    private function updateHashIfRequired($password)
511
 
    {
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]);
516
 
        }
517
516
    }
518
517
 
519
518
    /**
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);
 
607
            $event->save();
609
608
        }
610
609
        foreach ($this->layouts as $layout) {
611
610
            /* @var Layout $layout */
618
617
            $campaign->save();
619
618
        }
620
619
 
621
 
 
622
 
        // Reassign resolutions
623
 
        $this->getStore()->update('UPDATE `resolution` SET userId = :userId WHERE userId = :oldUserId', [
624
 
            'userId' => $user->userId,
625
 
            'oldUserId' => $this->userId
626
 
        ]);
627
 
 
628
620
        // Load again
629
621
        $this->loaded = false;
630
622
        $this->load(true);
637
629
     */
638
630
    public function validate()
639
631
    {
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.'));
642
634
 
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.'));
645
637
 
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.'));
648
640
 
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.'));
651
643
 
652
644
        try {
653
645
            $user = $this->userFactory->getByName($this->userName);
654
646
 
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.'));
657
649
        }
658
650
        catch (NotFoundException $e) {
659
651
 
663
655
            $this->pageFactory->getById($this->homePageId);
664
656
        }
665
657
        catch (NotFoundException $e) {
666
 
            throw new InvalidArgumentException(__('Selected home page does not exist'), 'homePageId');
 
658
            throw new \InvalidArgumentException(__('Selected home page does not exist'));
667
659
        }
668
660
    }
669
661
 
754
746
        }
755
747
 
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]);
760
751
    }
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);
988
 
 
989
 
                    $this->permissionCache[$entity][$permission->objectId] = $new;
990
979
                }
991
980
                else
992
981
                    $this->permissionCache[$entity][$permission->objectId] = $permission;
1002
991
     */
1003
992
    private function checkObjectCompatibility($object)
1004
993
    {
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'));
1007
996
    }
1008
997
 
1027
1016
            return $this->permissionFactory->getFullPermissions();
1028
1017
 
1029
1018
        // Get the permissions for that entity
1030
 
        $permissions = $this->loadPermissions($object->permissionsClass());
 
1019
        $permissions = $this->loadPermissions(get_class($object));
1031
1020
 
1032
1021
        // Check to see if our object is in the list
1033
1022
        if (array_key_exists($object->getId(), $permissions))
1056
1045
            return true;
1057
1046
 
1058
1047
        // Get the permissions for that entity
1059
 
        $permissions = $this->loadPermissions($object->permissionsClass());
 
1048
        $permissions = $this->loadPermissions(get_class($object));
1060
1049
 
1061
1050
        // Check to see if our object is in the list
1062
1051
        if (array_key_exists($object->getId(), $permissions))
1114
1103
            return true;
1115
1104
 
1116
1105
        // Get the permissions for that entity
1117
 
        $permissions = $this->loadPermissions($object->permissionsClass());
 
1106
        $permissions = $this->loadPermissions(get_class($object));
1118
1107
 
1119
1108
        // Check to see if our object is in the list
1120
1109
        if (array_key_exists($object->getId(), $permissions))
1154
1143
    }
1155
1144
 
1156
1145
    /**
1157
 
     * Is a super admin
1158
 
     * @return bool
1159
 
     */
1160
 
    public function isSuperAdmin()
1161
 
    {
1162
 
        return ($this->getUserTypeId() == 1);
1163
 
    }
1164
 
 
1165
 
    /**
1166
 
     * Is Group Admin
1167
 
     * @return bool
1168
 
     */
1169
 
    public function isGroupAdmin()
1170
 
    {
1171
 
       return ($this->getUserTypeId() == 2);
1172
 
    }
1173
 
 
1174
 
    /**
1175
1146
     * Is this users library quota full
1176
1147
     * @throws LibraryFullException when the library is full or cannot be determined
1177
1148
     */
1234
1205
    }
1235
1206
 
1236
1207
    /**
 
1208
     * Password hashing with PBKDF2.
 
1209
     * Author: havoc AT defuse.ca
 
1210
     * www: https://defuse.ca/php-pbkdf2.htm
 
1211
     * @param string $password
 
1212
     * @return string
 
1213
     */
 
1214
    private function createHash($password)
 
1215
    {
 
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,
 
1221
            $password,
 
1222
            $salt,
 
1223
            PBKDF2_ITERATIONS,
 
1224
            PBKDF2_HASH_BYTES,
 
1225
            true
 
1226
        ));
 
1227
    }
 
1228
 
 
1229
    /**
 
1230
     * Compares two strings $a and $b in length-constant time.
 
1231
     * @param string $a
 
1232
     * @param string $b
 
1233
     * @return bool
 
1234
     */
 
1235
    private function slowEquals($a, $b)
 
1236
    {
 
1237
        $diff = strlen($a) ^ strlen($b);
 
1238
        for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
 
1239
        {
 
1240
            $diff |= ord($a[$i]) ^ ord($b[$i]);
 
1241
        }
 
1242
        return $diff === 0;
 
1243
    }
 
1244
 
 
1245
    /**
1237
1246
     * Tests the supplied password against the password policy
1238
1247
     * @param string $password
1239
1248
     */
1251
1260
                throw new \InvalidArgumentException($policyError);
1252
1261
        }
1253
1262
    }
 
1263
 
 
1264
    /**
 
1265
     * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
 
1266
     *
 
1267
     * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
 
1268
     *
 
1269
     * This implementation of PBKDF2 was originally created by https://defuse.ca
 
1270
     * With improvements by http://www.variations-of-shadow.com
 
1271
     *
 
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.
 
1279
     */
 
1280
    public function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
 
1281
    {
 
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.');
 
1287
 
 
1288
        $hash_length = strlen(hash($algorithm, "", true));
 
1289
        $block_count = ceil($key_length / $hash_length);
 
1290
 
 
1291
        $output = "";
 
1292
        for ($i = 1; $i <= $block_count; $i++) {
 
1293
            // $i encoded as 4 bytes, big endian.
 
1294
            $last = $salt . pack("N", $i);
 
1295
            // first iteration
 
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));
 
1300
            }
 
1301
            $output .= $xorsum;
 
1302
        }
 
1303
 
 
1304
        if ($raw_output)
 
1305
            return substr($output, 0, $key_length);
 
1306
        else
 
1307
            return bin2hex(substr($output, 0, $key_length));
 
1308
    }
1254
1309
}