1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Field;
6:
7: use Atk4\Data\Exception;
8: use Atk4\Data\Field;
9: use Atk4\Data\Model;
10:
11: class PasswordField extends Field
12: {
13: /** @var int */
14: public $minLength = 8;
15:
16: public function normalizePassword(string $password, bool $forVerifyOnly): string
17: {
18: $password = (new Field(['type' => 'string']))->normalize($password);
19:
20: if (!preg_match('~^\P{C}+$~u', $password) || $this->hashPasswordIsHashed($password)) {
21: throw new Exception('Invalid password');
22: } elseif (!$forVerifyOnly && mb_strlen($password) < $this->minLength) {
23: throw new Exception('At least ' . $this->minLength . ' characters are required');
24: }
25:
26: return $password;
27: }
28:
29: public function hashPassword(string $password): string
30: {
31: $password = $this->normalizePassword($password, false);
32:
33: $hash = \password_hash($password, \PASSWORD_BCRYPT, ['cost' => 8]);
34: $e = false;
35: try {
36: if (!$this->hashPasswordIsHashed($hash) || !$this->hashPasswordVerify($hash, $password)) {
37: $e = null;
38: }
39: } catch (\Exception $e) {
40: }
41: if ($e !== false) {
42: throw new Exception('Unexpected error when hashing password', 0, $e);
43: }
44:
45: return $hash;
46: }
47:
48: public function hashPasswordVerify(string $hash, string $password): bool
49: {
50: $hash = $this->normalize($hash);
51: $password = $this->normalizePassword($password, true);
52:
53: return \password_verify($password, $hash);
54: }
55:
56: public function hashPasswordIsHashed(string $value): bool
57: {
58: try {
59: $value = parent::normalize($value) ?? '';
60: } catch (\Exception $e) {
61: }
62:
63: return \password_get_info($value)['algo'] === \PASSWORD_BCRYPT;
64: }
65:
66: #[\Override]
67: public function normalize($hash): ?string
68: {
69: $hash = parent::normalize($hash);
70:
71: if ($hash !== null && ($hash === '' || !$this->hashPasswordIsHashed($hash))) {
72: throw new Exception('Invalid password hash');
73: }
74:
75: return $hash;
76: }
77:
78: public function setPassword(Model $entity, string $password): self
79: {
80: $this->set($entity, $this->hashPassword($password));
81:
82: return $this;
83: }
84:
85: /**
86: * Returns true if the supplied password matches the stored hash.
87: */
88: public function verifyPassword(Model $entity, string $password): bool
89: {
90: $v = $this->get($entity);
91: if ($v === null) {
92: throw (new Exception('Password hash is null, verification is impossible'))
93: ->addMoreInfo('field', $this->shortName);
94: }
95:
96: return $this->hashPasswordVerify($v, $password);
97: }
98:
99: public function generatePassword(int $length = null): string
100: {
101: $charsAll = array_diff(array_merge(
102: range('0', '9'),
103: range('a', 'z'),
104: range('A', 'Z'),
105: ), ['0', 'o', 'O', '1', 'l', 'i', 'I']);
106:
107: $resArr = [];
108: for ($i = 0; $i < max(8, $length ?? $this->minLength); ++$i) {
109: $chars = array_values(array_diff($charsAll, array_slice($resArr, -4)));
110: $resArr[] = $chars[random_int(0, count($chars) - 1)];
111: }
112:
113: return $this->normalizePassword(implode('', $resArr), false);
114: }
115: }
116: