ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
ilBcryptPasswordEncoderTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21use org\bovigo\vfs;
22use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
23use PHPUnit\Framework\Attributes\DataProvider;
24use PHPUnit\Framework\Attributes\Depends;
25
27{
28 private const string VALID_COSTS = '08';
29 private const string PASSWORD = 'password';
30 private const string WRONG_PASSWORD = 'wrong_password';
31 private const string CLIENT_SALT = 'homer!12345_/';
32 private const string PASSWORD_SALT = 'salt';
33
34 private vfs\vfsStreamDirectory $testDirectory;
35 private string $testDirectoryUrl;
36
37 public function getTestDirectory(): vfs\vfsStreamDirectory
38 {
40 }
41
42 public function setTestDirectory(vfs\vfsStreamDirectory $testDirectory): void
43 {
44 $this->testDirectory = $testDirectory;
45 }
46
47 public function getTestDirectoryUrl(): string
48 {
50 }
51
52 public function setTestDirectoryUrl(string $testDirectoryUrl): void
53 {
54 $this->testDirectoryUrl = $testDirectoryUrl;
55 }
56
57 private function isVsfStreamInstalled(): bool
58 {
59 return class_exists('org\bovigo\vfs\vfsStreamWrapper');
60 }
61
62 private function skipIfvfsStreamNotSupported(): void
63 {
64 if (!$this->isVsfStreamInstalled()) {
65 $this->markTestSkipped('Skipped test, vfsStream (https://github.com/bovigo/vfsStream) required');
66 } else {
67 vfs\vfsStream::setup();
68 $this->setTestDirectory(vfs\vfsStream::newDirectory('test')->at(vfs\vfsStreamWrapper::getRoot()));
69 $this->setTestDirectoryUrl(vfs\vfsStream::url('root/test'));
70 }
71 }
72
76 public static function costsProvider(): array
77 {
78 $data = [];
79 for ($i = 4; $i <= 31; ++$i) {
80 $data[sprintf('Costs: %s', $i)] = [(string) $i];
81 }
82
83 return $data;
84 }
85
87 {
88 return new ilBcryptPasswordEncoder([
89 'data_directory' => $this->testDirectoryUrl
90 ]);
91 }
92
94 {
96
97 $security_flaw_ignoring_encoder = new ilBcryptPasswordEncoder([
98 'ignore_security_flaw' => true,
99 'data_directory' => $this->testDirectoryUrl
100 ]);
101 $this->assertTrue($security_flaw_ignoring_encoder->isSecurityFlawIgnored());
102
103 $security_flaw_respecting_encoder = new ilBcryptPasswordEncoder([
104 'ignore_security_flaw' => false,
105 'data_directory' => $this->testDirectoryUrl
106 ]);
107 $this->assertFalse($security_flaw_respecting_encoder->isSecurityFlawIgnored());
108
109 $encoder = new ilBcryptPasswordEncoder([
110 'cost' => self::VALID_COSTS,
111 'data_directory' => $this->testDirectoryUrl
112 ]);
113 $this->assertInstanceOf(ilBcryptPasswordEncoder::class, $encoder);
114 $this->assertSame(self::VALID_COSTS, $encoder->getCosts());
115 $this->assertFalse($encoder->isSecurityFlawIgnored());
116 $encoder->setClientSalt(self::CLIENT_SALT);
117
118 return $encoder;
119 }
120
121 #[Depends('testInstanceCanBeCreated')]
123 {
124 $expected = '04';
125
126 $encoder->setCosts($expected);
127 $this->assertSame($expected, $encoder->getCosts());
128 }
129
130 #[Depends('testInstanceCanBeCreated')]
132 {
133 $this->expectException(ilPasswordException::class);
134 $encoder->setCosts('32');
135 }
136
137 #[Depends('testInstanceCanBeCreated')]
139 {
140 $this->expectException(ilPasswordException::class);
141 $encoder->setCosts('3');
142 }
143
144 #[DoesNotPerformAssertions]
145 #[Depends('testInstanceCanBeCreated')]
146 #[DataProvider('costsProvider')]
147 public function testCostsCanBeSetInRange(string $costs, ilBcryptPasswordEncoder $encoder): void
148 {
149 $encoder->setCosts($costs);
150 }
151
152 #[Depends('testInstanceCanBeCreated')]
156 $encoder->setCosts(self::VALID_COSTS);
157 $encoded_password = $encoder->encodePassword(self::PASSWORD, self::PASSWORD_SALT);
158 $this->assertTrue($encoder->isPasswordValid($encoded_password, self::PASSWORD, self::PASSWORD_SALT));
159 $this->assertFalse($encoder->isPasswordValid($encoded_password, self::WRONG_PASSWORD, self::PASSWORD_SALT));
160
161 return $encoder;
162 }
163
164 #[Depends('testInstanceCanBeCreated')]
167 ): void {
168 $this->expectException(ilPasswordException::class);
169 $encoder->setCosts(self::VALID_COSTS);
170 $encoder->encodePassword(str_repeat('a', 5000), self::PASSWORD_SALT);
171 }
172
173 #[Depends('testInstanceCanBeCreated')]
176 ): void {
177 $encoder->setCosts(self::VALID_COSTS);
178 $this->assertFalse($encoder->isPasswordValid('encoded', str_repeat('a', 5000), self::PASSWORD_SALT));
179 }
180
181 #[Depends('testInstanceCanBeCreated')]
182 public function testEncoderReliesOnSalts(ilBcryptPasswordEncoder $encoder): void
183 {
184 $this->assertTrue($encoder->requiresSalt());
185 }
186
187 #[Depends('testInstanceCanBeCreated')]
189 {
190 $this->assertFalse($encoder->requiresReencoding('hello'));
191 }
192
193 #[Depends('testInstanceCanBeCreated')]
194 public function testNameShouldBeBcrypt(ilBcryptPasswordEncoder $encoder): void
195 {
196 $this->assertSame('bcrypt', $encoder->getName());
197 }
198
200 {
201 $this->skipIfvfsStreamNotSupported();
202
203 $this->expectException(ilPasswordException::class);
204 $encoder = $this->getInstanceWithConfiguredDataDirectory();
205 $encoder->setClientSalt(null);
206 $encoder->setCosts(self::VALID_COSTS);
207 $encoder->encodePassword(self::PASSWORD, self::PASSWORD_SALT);
208 }
209
211 {
212 $this->skipIfvfsStreamNotSupported();
213
214 $this->expectException(ilPasswordException::class);
215 $encoder = $this->getInstanceWithConfiguredDataDirectory();
216 $encoder->setClientSalt(null);
217 $encoder->setCosts(self::VALID_COSTS);
218 $encoder->isPasswordValid('12121212', self::PASSWORD, self::PASSWORD_SALT);
219 }
220
222 {
223 $this->skipIfvfsStreamNotSupported();
224
225 $this->testDirectory->chmod(0777);
226 vfs\vfsStream::newFile(ilBcryptPasswordEncoder::SALT_STORAGE_FILENAME)->withContent(self::CLIENT_SALT)->at($this->testDirectory);
227
228 $encoder = $this->getInstanceWithConfiguredDataDirectory();
229 $this->assertSame(self::CLIENT_SALT, $encoder->getClientSalt());
230 }
231
233 {
234 $this->skipIfvfsStreamNotSupported();
235
236 $this->testDirectory->chmod(0777);
237
238 $encoder = $this->getInstanceWithConfiguredDataDirectory();
239 $this->assertNotNull($encoder->getClientSalt());
240 }
241
243 {
244 $this->skipIfvfsStreamNotSupported();
245
246 $this->expectException(ilPasswordException::class);
247 $this->testDirectory->chmod(0000);
248
249 $this->getInstanceWithConfiguredDataDirectory();
250 }
251
253 {
254 $this->skipIfvfsStreamNotSupported();
255
256 $encoder = $this->getInstanceWithConfiguredDataDirectory();
257 $encoder->setBackwardCompatibility(true);
258 $this->assertTrue($encoder->isBackwardCompatibilityEnabled());
259 $encoder->setBackwardCompatibility(false);
260 $this->assertFalse($encoder->isBackwardCompatibilityEnabled());
261 }
262
263 public function testBackwardCompatibility(): void
264 {
265 $this->skipIfvfsStreamNotSupported();
266
267 $encoder = $this->getInstanceWithConfiguredDataDirectory();
268 $encoder->setClientSalt(self::CLIENT_SALT);
269 $encoder->setBackwardCompatibility(true);
270
271 $encoded_password = $encoder->encodePassword(self::PASSWORD, self::PASSWORD_SALT);
272 $this->assertTrue($encoder->isPasswordValid($encoded_password, self::PASSWORD, self::PASSWORD_SALT));
273 $this->assertSame('$2a$', substr($encoded_password, 0, 4));
274
275 $another_encoder = $this->getInstanceWithConfiguredDataDirectory();
276 $another_encoder->setClientSalt(self::CLIENT_SALT);
277
278 $another_encoder->setBackwardCompatibility(false);
279 $another_encoded_password = $another_encoder->encodePassword(self::PASSWORD, self::PASSWORD_SALT);
280 $this->assertSame('$2y$', substr($another_encoded_password, 0, 4));
281 $this->assertTrue($another_encoder->isPasswordValid($encoded_password, self::PASSWORD, self::PASSWORD_SALT));
282 }
283
285 {
286 $this->skipIfvfsStreamNotSupported();
287
288 $this->expectException(ilPasswordException::class);
289 $encoder = $this->getInstanceWithConfiguredDataDirectory();
290 $encoder->setClientSalt(self::CLIENT_SALT);
291 $encoder->setBackwardCompatibility(true);
292 $encoder->encodePassword(self::PASSWORD . chr(195), self::PASSWORD_SALT);
293 }
294
295 #[DoesNotPerformAssertions]
297 {
298 $this->skipIfvfsStreamNotSupported();
299
300 $encoder = $this->getInstanceWithConfiguredDataDirectory();
301 $encoder->setClientSalt(self::CLIENT_SALT);
302 $encoder->setBackwardCompatibility(true);
303 $encoder->setIsSecurityFlawIgnored(true);
304 $encoder->encodePassword(self::PASSWORD . chr(195), self::PASSWORD_SALT);
305 }
306}
testCostsCanBeSetInRange(string $costs, ilBcryptPasswordEncoder $encoder)
testPasswordVerificationShouldFailIfTheRawPasswordExceedsTheSupportedLength(ilBcryptPasswordEncoder $encoder)
setTestDirectory(vfs\vfsStreamDirectory $testDirectory)
testPasswordShouldBeCorrectlyEncodedAndVerified(ilBcryptPasswordEncoder $encoder)
testCostsCanBeRetrievedWhenCostsAreSet(ilBcryptPasswordEncoder $encoder)
testCostsCannotBeSetBelowRange(ilBcryptPasswordEncoder $encoder)
testNoExceptionIfPasswordsContainA8BitCharacterAndBackwardCompatibilityIsEnabledWithIgnoredSecurityFlaw()
setTestDirectoryUrl(string $testDirectoryUrl)
testEncoderReliesOnSalts(ilBcryptPasswordEncoder $encoder)
testExceptionIsRaisedIfThePasswordExceedsTheSupportedLengthOnEncoding(ilBcryptPasswordEncoder $encoder)
testEncoderDoesNotSupportReencoding(ilBcryptPasswordEncoder $encoder)
testCostsCannotBeSetAboveRange(ilBcryptPasswordEncoder $encoder)
testNameShouldBeBcrypt(ilBcryptPasswordEncoder $encoder)
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+.
requiresSalt()
Returns whether the encoder requires a salt.
isPasswordValid(string $encoded, string $raw, string $salt)
Checks a raw password against an encoded password.
requiresReencoding(string $encoded)
Returns whether the encoded password needs to be re-encoded.
setIsSecurityFlawIgnored(bool $is_security_flaw_ignored)
Class for user password exception handling in ILIAS.