Skip to content

Commit 7eae1d4

Browse files
committed
added benchmark cost, sodium_compact and fixed options
1 parent 5d98210 commit 7eae1d4

File tree

12 files changed

+187
-90
lines changed

12 files changed

+187
-90
lines changed

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
# Released Notes
22

3-
## v3.0.0 - (2022-11-11)
3+
## v3.1.0 - (2024-02-xx)
4+
5+
### Added
6+
7+
- Added method to benchmark cost
8+
- Added microseconds in `usleep`
9+
- Added set cost using `getOptimalBcryptCost` class
10+
- Added `password_get_info` in `verifyHash`
11+
- Added `paragonie/sodium_compat` component
12+
13+
### Fixed
14+
15+
- Fixed usleep in `verifyHash` method
16+
- Fixed options at `password_hash`
17+
18+
## Removed
19+
20+
- Removed `HashException` class
21+
22+
-----------------------------------------------------------
23+
24+
## v3.0.0 - (2023-11-11)
425

526
### Added
627

README.md

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,20 @@ $hash = $password->createHash('my_password')->verifyHash();
104104
var_dump($res);
105105
```
106106

107+
To make timing attacks more difficult, the `verifyHash` method waits 0.25 seconds (250000 microseconds) to return the value. You can change this time by changing the third parameter.
108+
109+
```php
110+
# First way
111+
$hash = $password->createHash('my_password')->getHash();
112+
$res = $password->verifyHash('my_password', $hash, 300000);
113+
114+
# Second way
115+
$hash = $password->createHash('my_password')->verifyHash(wait_microseconds: 300000);
116+
117+
/** Return bool */
118+
var_dump($res);
119+
```
120+
107121
**NOTE: If you are using the settings passed in the constructor then you can ignore the code below.**
108122

109123
You can change the type of algorithm that will be used to check the hash.
@@ -116,14 +130,34 @@ $res = $password->useArgon2()->verifyHash('my_password', $hash);
116130
var_dump($res);
117131
```
118132

119-
If the encryption type has been changed, you can generate a new hash with the new encryption. The `needsHash()` method checks whether the reported hash needs to be regenerated. Otherwise, it will return false.
133+
## Needs Rehash
134+
135+
If the encryption type has been changed, you can generate a new hash with the new encryption. The `needsHash()` method checks whether the reported hash needs to be regenerated. Otherwise, it will return `false`.
120136

121137
```php
138+
/**
139+
* EXAMPLE 1
140+
*/
141+
$password = new SecurePassword();
122142
$hash = $password->useArgon2()->createHash('my_password')->getHash();
123143
$needs = $password->useDefault()->needsRehash('my_password', $hash);
124144

125-
/** Return bool or string */
126-
var_dump($res);
145+
/** Return string */
146+
var_dump($needs);
147+
148+
/**
149+
* EXAMPLE 2
150+
*/
151+
152+
$hash = $password->createHash('my_password')->getHash();
153+
154+
$password = new SecurePassword([
155+
'algo' => HashAlgorithm::BCRYPT
156+
]);
157+
$needs = $password->needsRehash('my_password', $hash);
158+
159+
/** Return false */
160+
var_dump($needs);
127161
```
128162

129163
## Adding options
@@ -152,6 +186,8 @@ $hash = $password->useArgon2(true, PASSWORD_ARGON2_DEFAULT_MEMORY_COST, PASSWORD
152186

153187
## Using OpenSSL and Sodium encryption
154188

189+
Secure Password has the component [paragonie/sodium_compat](https://github.com/paragonie/sodium_compat). Therefore, it is not necessary to use the Sodium library in PECL format.
190+
155191
You can use OpenSSL and Sodium encryption using the `Encryption` class:
156192

157193
```php
@@ -213,11 +249,12 @@ $password->setPepper('new_pepper', 'sodium');
213249
Here's a quick little function that will help you determine what cost parameter you should be using for your server to make sure you are within this range.
214250

215251
```php
216-
$password = new SecurePassword();
217-
$cost = $password->getOptimalBcryptCost();
252+
$optimal_cost = SecurePassword::getOptimalBcryptCost('my_password');
218253

219-
/** Return int */
220-
var_dump($cost);
254+
$password = new SecurePassword([
255+
'cost' => $optimal_cost
256+
]);
257+
$hash = $password->createHash('my_password')->getHash();
221258
```
222259

223260
## License

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"php-password"
99
],
1010
"require": {
11-
"php": "^8.2"
11+
"php": "^8.2",
12+
"paragonie/sodium_compat": "^1.20"
1213
},
1314
"require-dev": {
1415
"phpunit/phpunit": "^10"

src/Encrypt/Adapter/OpenSslEncryption.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(string $key)
3737
if ($key === '') {
3838
throw new \InvalidArgumentException('The key should not be empty string.');
3939
}
40-
40+
4141
$this->iv = openssl_random_pseudo_bytes($this->ivBytes($this->cipher));
4242
$this->key = hash('sha512', $key);
4343
}

src/Encrypt/Adapter/SodiumEncryption.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function __construct(string $key)
2121
if ($key === '') {
2222
throw new \InvalidArgumentException('The key should not be empty string.');
2323
}
24-
24+
2525
if (!function_exists('sodium_crypto_secretbox_keygen')) {
2626
throw new \Exception('The sodium php extension does not installed or enabled', 500);
2727
}

src/Encrypt/Encryption.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ class Encryption
1111
*/
1212
public function __construct(
1313
private AbstractAdapterInterface $adapter
14-
)
15-
{
16-
if ($this->adapter === null) {
17-
throw new \InvalidArgumentException('The adapter class should not be null.');
18-
}
14+
) {
1915
}
2016

2117
/**

src/HashAlgorithm.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ public function useArgon2(
6565
'threads' => $threads
6666
];
6767

68+
$this->algo = self::ARGON2I;
69+
6870
if ($use_argon2d == true) {
6971
$this->algo = self::ARGON2ID;
70-
} else {
71-
$this->algo = self::ARGON2I;
7272
}
7373

7474
return $this;

src/HashException.php

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/PepperTrait.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
namespace SecurePassword;
44

5-
use SecurePassword\Encrypt\Adapter\OpenSslEncryption;
6-
use SecurePassword\Encrypt\Adapter\SodiumEncryption;
75
use SecurePassword\Encrypt\Encryption;
6+
use SecurePassword\Encrypt\Adapter\{OpenSslEncryption, SodiumEncryption};
87

98
trait PepperTrait
109
{
@@ -57,7 +56,6 @@ private function useSodium(): mixed
5756
return $encryption->encrypt($this->pepper);
5857
}
5958

60-
6159
/**
6260
* Adds a secret entry (commonly called `pepper`) to the password
6361
*

src/SecurePassword.php

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
namespace SecurePassword;
44

5-
use SecurePassword\PepperTrait;
6-
use SecurePassword\HashAlgorithm;
7-
use SecurePassword\HashException;
5+
use SecurePassword\{
6+
PepperTrait,
7+
HashAlgorithm
8+
};
89

910
class SecurePassword extends HashAlgorithm
1011
{
@@ -20,27 +21,25 @@ class SecurePassword extends HashAlgorithm
2021
*/
2122
private string $password = "";
2223

24+
/**
25+
* @var int|null|null
26+
*/
27+
private ?int $cost_min_ms = null;
28+
2329
/**
2430
* @param array $config
2531
*/
2632
public function __construct(
2733
private array $config = [
2834
"algo" => HashAlgorithm::DEFAULT,
29-
"cost" => "",
35+
"cost" => "10",
3036
"memory_cost" => "",
3137
"time_cost" => "",
3238
"threads" => ""
3339
]
3440
) {
35-
foreach ($config as $key => $value) {
36-
if (!isset($this->config[$key])) {
37-
throw new HashException("Key '$key' not exists");
38-
}
39-
40-
$this->options = $this->config;
41-
$this->algo = $this->config['algo'];
42-
}
43-
41+
$this->options = $this->config;
42+
$this->algo = $this->config['algo'];
4443
$this->setPepper();
4544
}
4645

@@ -54,15 +53,15 @@ public function __construct(
5453
public function createHash(string $password): SecurePassword
5554
{
5655
$this->password = $password;
57-
5856
$pwd_peppered = $this->passwordPeppered($this->password);
59-
60-
$this->pwd_hashed = password_hash($pwd_peppered, $this->algo);
57+
$this->pwd_hashed = password_hash($pwd_peppered, $this->algo, $this->options);
6158

6259
return $this;
6360
}
6461

6562
/**
63+
* Return password hash
64+
*
6665
* @return string
6766
*/
6867
public function getHash(): string
@@ -71,6 +70,10 @@ public function getHash(): string
7170
}
7271

7372
/**
73+
* Returns information about the given hash
74+
*
75+
* @param string|null $hash
76+
*
7477
* @return mixed
7578
*/
7679
public function getHashInfo(): mixed
@@ -83,10 +86,11 @@ public function getHashInfo(): mixed
8386
*
8487
* @param null|string $password
8588
* @param null|string $hash
89+
* @param int $wait
8690
*
8791
* @return bool
8892
*/
89-
public function verifyHash(?string $password = null, ?string $hash = null): bool
93+
public function verifyHash(?string $password = null, ?string $hash = null, int $wait_microseconds = 250000): bool
9094
{
9195
if (is_null($password)) {
9296
$password = $this->password;
@@ -96,33 +100,31 @@ public function verifyHash(?string $password = null, ?string $hash = null): bool
96100
$hash = $this->pwd_hashed;
97101
}
98102

99-
$pph_strt = microtime(true);
100-
$pwd_peppered = $this->passwordPeppered($password);
101-
102-
if (password_verify($pwd_peppered, $hash)) {
103-
try {
104-
return true;
105-
} finally {
106-
$end = (microtime(true) - $pph_strt);
107-
$wait = bcmul((1 - $end), 1000000); // usleep(250000) 1/4 of a second
108-
usleep($wait);
109-
}
103+
if (password_get_info($hash)['algoName'] === 'unknown') {
104+
return false;
110105
}
111106

112-
return false;
107+
$pwd_peppered = $this->passwordPeppered($password);
108+
$res = password_verify($pwd_peppered, $hash);
109+
usleep($wait_microseconds);
110+
111+
return $res;
113112
}
114113

115114
/**
116115
* Here's a quick little function that will help you determine what cost parameter you should be
117116
* using for your server to make sure you are within this range.
118117
*
119-
* @param int $min_ms
120118
* @param string $password
119+
* @param string $crypt
120+
* @param int $min_ms
121121
*
122122
* @return int
123123
*/
124-
public function getOptimalBcryptCost(int $min_ms = 250, string $password = "test"): int
125-
{
124+
public static function getOptimalBcryptCost(
125+
string $password,
126+
int $min_ms = 250
127+
): int {
126128
for ($i = 4; $i < 31; $i++) {
127129
$time_start = microtime(true);
128130
password_hash($password, PASSWORD_BCRYPT, ['cost' => $i]);
@@ -146,11 +148,35 @@ public function getOptimalBcryptCost(int $min_ms = 250, string $password = "test
146148
public function needsRehash(string $password, string $hash): string|false
147149
{
148150
if (password_needs_rehash($hash, $this->algo)) {
149-
$newHash = $this->createHash($password)->getHash();
150-
151-
return $newHash;
151+
return $this->createHash($password)->getHash();
152152
}
153153

154154
return false;
155155
}
156+
157+
/**
158+
* This code will benchmark your server to determine how high of a cost you can
159+
* afford. You want to set the highest cost that you can without slowing down
160+
* you server too much. 10 is a good baseline, and more is good if your servers
161+
* are fast enough. The code below aims for ≤ 350 milliseconds stretching time,
162+
* which is an appropriate delay for systems handling interactive logins.
163+
*
164+
* @param string $password
165+
*
166+
* @return int
167+
*/
168+
public static function benchmarkCost(string $password): int
169+
{
170+
$timeTarget = 0.350; // 350 milliseconds
171+
$cost = 10;
172+
173+
do {
174+
$cost++;
175+
$start = microtime(true);
176+
password_hash($password, PASSWORD_BCRYPT, ["cost" => $cost]);
177+
$end = microtime(true);
178+
} while (($end - $start) < $timeTarget);
179+
180+
return $cost;
181+
}
156182
}

0 commit comments

Comments
 (0)