Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 3.1.0 #23

Merged
merged 2 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 1 addition & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,13 @@ name: CI
on: [push]

jobs:
old:
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: ['ubuntu-16.04']
php-versions: ['7.0']
phpunit-versions: ['7.5.20']
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, intl
ini-values: post_max_size=256M, max_execution_time=180
tools: psalm, phpunit:${{ matrix.phpunit-versions }}

- name: Fix permissions
run: sudo chmod -R 0777 .

- name: Install dependencies
run: composer self-update --1; composer install

- name: PHPUnit tests
uses: php-actions/phpunit@v2
with:
memory_limit: 256M

moderate:
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: ['ubuntu-latest']
php-versions: ['7.1', '7.2', '7.3']
php-versions: ['7.3']
phpunit-versions: ['latest']
steps:
- name: Checkout
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# Version 3.1.0 (2021-06-02)

* Added `needsRehash()` method.
* Added support for `$hashOptions` in `hashAndEncrypt()` to support
custom bcrypt costs. (This can also be used to support custom Argon2id
parameters, should the default ever change in PHP.)

# Version 3.0.3 (2021-06-02)

* Support PHP 8.
* The previous tag (v3.0.2) was erroneous and erased.

# Version 3.0.1 (2016-05-20)

* Fixed `autoload.php`

# Version 3.0.0 (2016-05-18)

* Set minimum PHP version to 7.0
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ if (isset($_POST['password'])) {
}
```

### Determine if a re-hash is necessary

```php
use ParagonIE\PasswordLock\PasswordLock;
/**
* @var string $encryptedPwhash
* @var Defuse\Crypto\Key $key
*/

if (PasswordLock::needsRehash($encryptedPwhash, $key)) {
// Recalculate PasswordLock::hashAndEncrypt()
}
```

### Re-encrypt a hash with a different encryption key

```php
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
]
},
"require": {
"php": "^7|^8",
"php": "^7.3|^8",
"defuse/php-encryption": "^2",
"paragonie/constant_time_encoding": "^2"
},
Expand Down
78 changes: 74 additions & 4 deletions src/PasswordLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,47 @@
*/
class PasswordLock
{
/**
* @ref https://www.php.net/manual/en/function.password-hash.php
*/
const OPTIONS_DEFAULT_BCRYPT = ['cost' => 12];
const OPTIONS_DEFAULT_ARGON2ID = [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 1
];

/**
* 1. Hash password using bcrypt-base64-SHA256
* 2. Encrypt-then-MAC the hash
*
* @param string $password
* @param Key $aesKey
* @param ?array $hashOptions
* @return string
*
* @throws EnvironmentIsBrokenException
* @throws \InvalidArgumentException
* @psalm-suppress InvalidArgument
*/
public static function hashAndEncrypt(string $password, Key $aesKey): string
{
public static function hashAndEncrypt(
string $password,
Key $aesKey,
?array $hashOptions = null
): string {
if (is_null($hashOptions)) {
$hashOptions = static::getDefaultOptions();
}
if (array_key_exists('salt', $hashOptions)) {
throw new \InvalidArgumentException('Explicit salts are unsupported.');
}
/** @var string $hash */
$hash = \password_hash(
Base64::encode(
\hash('sha384', $password, true)
),
PASSWORD_DEFAULT
PASSWORD_DEFAULT,
$hashOptions
);
if (!\is_string($hash)) {
throw new EnvironmentIsBrokenException("Unknown hashing error.");
Expand All @@ -53,7 +75,11 @@ public static function hashAndEncrypt(string $password, Key $aesKey): string
* @throws EnvironmentIsBrokenException
* @throws WrongKeyOrModifiedCiphertextException
*/
public static function decryptAndVerifyLegacy(string $password, string $ciphertext, string $aesKey): bool
public static function decryptAndVerifyLegacy(
string $password,
string $ciphertext,
string $aesKey
): bool
{
if (Binary::safeStrlen($aesKey) !== 16) {
throw new \InvalidArgumentException("Encryption keys must be 16 bytes long");
Expand Down Expand Up @@ -102,6 +128,50 @@ public static function decryptAndVerify(string $password, string $ciphertext, Ke
);
}

/**
* @return array<string, int>
*
* @psalm-suppress TypeDoesNotContainType
*/
protected static function getDefaultOptions(): array
{
// Future-proofing:
if (PASSWORD_DEFAULT === PASSWORD_ARGON2ID) {
return self::OPTIONS_DEFAULT_ARGON2ID;
}
return self::OPTIONS_DEFAULT_BCRYPT;
}

/**
* Decrypt the ciphertext and ascertain if the stored password needs to be rehashed?
*
* @param string $ciphertext
* @param Key $aesKey
* @param ?array $hashOptions
* @return bool
*
* @throws EnvironmentIsBrokenException
* @throws WrongKeyOrModifiedCiphertextException
*/
public static function needsRehash(
string $ciphertext,
Key $aesKey,
?array $hashOptions = null
): bool {
if (is_null($hashOptions)) {
$hashOptions = static::getDefaultOptions();
}
$hash = Crypto::decrypt(
$ciphertext,
$aesKey
);
if (!\is_string($hash)) {
throw new EnvironmentIsBrokenException("Unknown hashing error.");
}
/** @psalm-suppress InvalidArgument */
return password_needs_rehash($hash, PASSWORD_DEFAULT, $hashOptions);
}

/**
* Key rotation method -- decrypt with your old key then re-encrypt with your new key
*
Expand Down
9 changes: 9 additions & 0 deletions tests/PasswordLockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@ public function testBitflip()
}
$this->assertTrue($failed, 'Bitflips should break the decryption');
}

public function testNeedsRehash()
{
$lowCost = ['cost' => 8];
$key = Key::createNewRandomKey();
$password = PasswordLock::hashAndEncrypt('YELLOW SUBMARINE', $key, $lowCost);
$this->assertTrue(PasswordLock::needsRehash($password, $key));
$this->assertFalse(PasswordLock::needsRehash($password, $key, $lowCost));
}
}