ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
ilBcryptPasswordEncoderTest.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
21 use org\bovigo\vfs;
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  {
39  return $this->testDirectory;
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')]
154  ilBcryptPasswordEncoder $encoder
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')]
166  ilBcryptPasswordEncoder $encoder
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')]
175  ilBcryptPasswordEncoder $encoder
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  {
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  {
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  {
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  {
235 
236  $this->testDirectory->chmod(0777);
237 
238  $encoder = $this->getInstanceWithConfiguredDataDirectory();
239  $this->assertNotNull($encoder->getClientSalt());
240  }
241 
243  {
245 
246  $this->expectException(ilPasswordException::class);
247  $this->testDirectory->chmod(0000);
248 
250  }
251 
253  {
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  {
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  {
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  {
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 }
testCostsCannotBeSetAboveRange(ilBcryptPasswordEncoder $encoder)
requiresSalt()
Returns whether the encoder requires a salt.
testCostsCanBeRetrievedWhenCostsAreSet(ilBcryptPasswordEncoder $encoder)
encodePassword(string $raw, string $salt)
Encodes the raw password.
testEncoderDoesNotSupportReencoding(ilBcryptPasswordEncoder $encoder)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
setTestDirectory(vfs\vfsStreamDirectory $testDirectory)
testPasswordShouldBeCorrectlyEncodedAndVerified(ilBcryptPasswordEncoder $encoder)
testExceptionIsRaisedIfThePasswordExceedsTheSupportedLengthOnEncoding(ilBcryptPasswordEncoder $encoder)
requiresReencoding(string $encoded)
Returns whether the encoded password needs to be re-encoded.
testPasswordVerificationShouldFailIfTheRawPasswordExceedsTheSupportedLength(ilBcryptPasswordEncoder $encoder)
testCostsCanBeSetInRange(string $costs, ilBcryptPasswordEncoder $encoder)
getName()
Returns a unique name/id of the concrete password encoder.
testCostsCannotBeSetBelowRange(ilBcryptPasswordEncoder $encoder)
testNameShouldBeBcrypt(ilBcryptPasswordEncoder $encoder)
testEncoderReliesOnSalts(ilBcryptPasswordEncoder $encoder)
setTestDirectoryUrl(string $testDirectoryUrl)
isPasswordValid(string $encoded, string $raw, string $salt)
Checks a raw password against an encoded password.
testNoExceptionIfPasswordsContainA8BitCharacterAndBackwardCompatibilityIsEnabledWithIgnoredSecurityFlaw()