polygon-website-foss/api/private/classes/ParagonIE/PasswordLock/PasswordLock.php

221 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
namespace ParagonIE\PasswordLock;
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException;
use Defuse\Crypto\Key;
use ParagonIE\ConstantTime\Base64;
use ParagonIE\ConstantTime\Binary;
/**
* Class PasswordLock
* @package ParagonIE\PasswordLock
*/
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,
?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)
),
// PROJECT POLYGON MODIFICATION
// PASSWORD_DEFAULT,
PASSWORD_ARGON2ID,
// END PROJECT POLYGON MODIFICATION
$hashOptions
);
if (!\is_string($hash)) {
throw new EnvironmentIsBrokenException("Unknown hashing error.");
}
return Crypto::encrypt($hash, $aesKey);
}
/**
* 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash
* 2. Verify that the password matches the hash
*
* @param string $password
* @param string $ciphertext
* @param string $aesKey - must be exactly 16 bytes
* @return bool
*
* @throws \InvalidArgumentException
* @throws EnvironmentIsBrokenException
* @throws WrongKeyOrModifiedCiphertextException
*/
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");
}
$hash = Crypto::legacyDecrypt(
$ciphertext,
$aesKey
);
if (!\is_string($hash)) {
throw new EnvironmentIsBrokenException("Unknown hashing error.");
}
return \password_verify(
Base64::encode(
\hash('sha256', $password, true)
),
$hash
);
}
/**
* 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash
* 2. Verify that the password matches the hash
*
* @param string $password
* @param string $ciphertext
* @param Key $aesKey
* @return bool
*
* @throws EnvironmentIsBrokenException
* @throws WrongKeyOrModifiedCiphertextException
*/
public static function decryptAndVerify(string $password, string $ciphertext, Key $aesKey): bool
{
$hash = Crypto::decrypt(
$ciphertext,
$aesKey
);
if (!\is_string($hash)) {
throw new EnvironmentIsBrokenException("Unknown hashing error.");
}
return \password_verify(
Base64::encode(
\hash('sha384', $password, true)
),
$hash
);
}
/**
* @return array<string, int>
*
* @psalm-suppress TypeDoesNotContainType
*/
protected static function getDefaultOptions(): array
{
// PROJECT POLYGON MODIFICATION
// Future-proofing:
// if (PASSWORD_DEFAULT === PASSWORD_ARGON2ID) {
return self::OPTIONS_DEFAULT_ARGON2ID;
// }
// return self::OPTIONS_DEFAULT_BCRYPT;
// END PROJECT POLYGON MODIFICATION
}
/**
* 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
*
* @param string $ciphertext
* @param Key $oldKey
* @param Key $newKey
* @return string
*
* @throws EnvironmentIsBrokenException
* @throws WrongKeyOrModifiedCiphertextException
*/
public static function rotateKey(string $ciphertext, Key $oldKey, Key $newKey): string
{
$plaintext = Crypto::decrypt($ciphertext, $oldKey);
return Crypto::encrypt($plaintext, $newKey);
}
/**
* For migrating from an older version of the library
*
* @param string $password
* @param string $ciphertext
* @param string $oldKey
* @param Key $newKey
* @return string
* @throws \Exception
*/
public static function upgradeFromVersion1(
string $password,
string $ciphertext,
string $oldKey,
Key $newKey
): string {
if (!self::decryptAndVerifyLegacy($password, $ciphertext, $oldKey)) {
throw new \Exception(
'The correct password is necessary for legacy migration.'
);
}
$plaintext = Crypto::legacyDecrypt($ciphertext, $oldKey);
return self::hashAndEncrypt($plaintext, $newKey);
}
}