ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilBcryptPasswordEncoder.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
28{
30 private const int MIN_SALT_SIZE = 16;
31
33 public const string SALT_STORAGE_FILENAME = 'pwsalt.txt';
34
35 private ?string $client_salt = null;
36 private bool $is_security_flaw_ignored = false;
37 private bool $backward_compatibility = false;
38 private string $data_directory = '';
39
44 public function __construct(array $config = [])
45 {
46 foreach ($config as $key => $value) {
47 $key = strtolower($key);
48 if ($key === 'ignore_security_flaw') {
49 $this->setIsSecurityFlawIgnored($value);
50 } elseif ($key === 'data_directory') {
51 $this->setDataDirectory($value);
52 }
53 }
54
55 parent::__construct($config);
56 $this->readClientSalt();
57 }
58
59 private function isBcryptSupported(): bool
60 {
61 return PHP_VERSION_ID >= 50307;
62 }
63
64 public function getDataDirectory(): string
65 {
67 }
68
69 public function setDataDirectory(string $data_directory): void
70 {
71 $this->data_directory = $data_directory;
72 }
73
74 public function isBackwardCompatibilityEnabled(): bool
75 {
77 }
78
83 {
84 $this->backward_compatibility = $backward_compatibility;
85 }
86
87 public function isSecurityFlawIgnored(): bool
88 {
90 }
91
93 {
94 $this->is_security_flaw_ignored = $is_security_flaw_ignored;
95 }
96
97 public function getClientSalt(): ?string
98 {
99 return $this->client_salt;
100 }
101
102 public function setClientSalt(?string $client_salt): void
103 {
104 $this->client_salt = $client_salt;
105 }
106
107 public function encodePassword(string $raw, string $salt): string
108 {
109 if (!$this->client_salt) {
110 throw new ilPasswordException('Missing client salt.');
111 }
112
113 if ($this->isPasswordTooLong($raw)) {
114 throw new ilPasswordException('Invalid password.');
115 }
116
117 return $this->encode($raw, $salt);
118 }
119
120 public function isPasswordValid(string $encoded, string $raw, string $salt): bool
121 {
122 if (!$this->client_salt) {
123 throw new ilPasswordException('Missing client salt.');
124 }
125
126 return !$this->isPasswordTooLong($raw) && $this->check($encoded, $raw, $salt);
127 }
128
129 public function getName(): string
130 {
131 return 'bcrypt';
132 }
133
134 public function requiresSalt(): bool
135 {
136 return true;
137 }
138
139 public function requiresReencoding(string $encoded): bool
140 {
141 return false;
142 }
143
144 private function encode(string $raw, string $userSecret): string
145 {
146 $clientSecret = $this->client_salt;
147 $hashedPassword = hash_hmac(
148 'whirlpool',
149 str_pad($raw, strlen($raw) * 4, sha1($userSecret), STR_PAD_BOTH),
150 $clientSecret ?? '',
151 true
152 );
153 $salt = substr(
154 str_shuffle(str_repeat('./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 22)),
155 0,
156 22
157 );
158
163 if ($this->isBcryptSupported() && !$this->backward_compatibility) {
164 $prefix = '$2y$';
165 } else {
166 $prefix = '$2a$';
167 // check if the password contains 8-bit character
168 if (!$this->is_security_flaw_ignored && preg_match('#[\x80-\xFF]#', $raw)) {
169 throw new ilPasswordException(
170 'The bcrypt implementation used by PHP can contain a security flaw ' .
171 'using passwords with 8-bit characters. ' .
172 'We suggest to upgrade to PHP 5.3.7+ or use passwords with only 7-bit characters.'
173 );
174 }
175 }
176
177 $saltedPassword = crypt($hashedPassword, $prefix . $this->getCosts() . '$' . $salt);
178 if (strlen($saltedPassword) <= 13) {
179 throw new ilPasswordException('Error during the bcrypt generation');
180 }
181
182 return $saltedPassword;
183 }
184
185 private function check(string $encoded, string $raw, string $salt): bool
186 {
187 $hashedPassword = hash_hmac(
188 'whirlpool',
189 str_pad($raw, strlen($raw) * 4, sha1($salt), STR_PAD_BOTH),
190 (string) $this->client_salt,
191 true
192 );
193
194 return $this->comparePasswords($encoded, crypt($hashedPassword, substr($encoded, 0, 30)));
195 }
196
197 public function getClientSaltLocation(): string
198 {
199 return $this->data_directory . '/' . self::SALT_STORAGE_FILENAME;
200 }
201
202 private function readClientSalt(): void
203 {
204 if (is_file($this->getClientSaltLocation()) && is_readable($this->getClientSaltLocation())) {
205 $contents = file_get_contents($this->getClientSaltLocation());
206 if ($contents !== false && trim($contents) !== '') {
207 $this->setClientSalt($contents);
208 }
209 } else {
210 $this->generateClientSalt();
211 $this->storeClientSalt();
212 }
213 }
214
215 private function generateClientSalt(): void
216 {
217 $this->setClientSalt(
218 substr(str_replace('+', '.', base64_encode(ilPasswordUtils::getBytes(self::MIN_SALT_SIZE))), 0, 22)
219 );
220 }
221
222 private function storeClientSalt(): void
223 {
225
226 set_error_handler(static function (int $severity, string $message, string $file, int $line): never {
227 throw new ErrorException($message, $severity, $severity, $file, $line);
228 });
229
230 try {
231 $result = file_put_contents($location, $this->client_salt);
232 if (!$result) {
233 throw new ilPasswordException(sprintf(
234 'Could not store the client salt in: %s. Please contact an administrator.',
236 ));
237 }
238 } catch (Exception $e) {
239 throw new ilPasswordException(sprintf(
240 'Could not store the client salt in: %s. Please contact an administrator.',
242 ), $e->getCode(), $e);
243 } finally {
244 restore_error_handler();
245 }
246 }
247}
$location
Definition: buildRTE.php:22
comparePasswords(string $knownString, string $userString)
Compares two passwords.
encodePassword(string $raw, string $salt)
Encodes the raw password.
getName()
Returns a unique name/id of the concrete password encoder.
setBackwardCompatibility(bool $backward_compatibility)
Set the backward compatibility $2a$ instead of $2y$ for PHP 5.3.7+.
encode(string $raw, string $userSecret)
requiresSalt()
Returns whether the encoder requires a salt.
isPasswordValid(string $encoded, string $raw, string $salt)
Checks a raw password against an encoded password.
setDataDirectory(string $data_directory)
requiresReencoding(string $encoded)
Returns whether the encoded password needs to be re-encoded.
check(string $encoded, string $raw, string $salt)
setIsSecurityFlawIgnored(bool $is_security_flaw_ignored)
Class for user password exception handling in ILIAS.
static getBytes(int $length)
Generate random bytes using OpenSSL or Mcrypt and mt_rand() as fallback.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
$message
Definition: xapiexit.php:31