<?php
declare(strict_types=1);
namespace ParagonIE\PasswordLock;
use \Defuse\Crypto\Crypto;
use \Defuse\Crypto\Key;
use \ParagonIE\ConstantTime\Base64;
use \ParagonIE\ConstantTime\Binary;
class PasswordLock
{
/**
* 1. Hash password using bcrypt-base64-SHA256
* 2. Encrypt-then-MAC the hash
*
* @param string $password
* @param Key $aesKey
* @return string
* @throws \Exception
* @throws \InvalidArgumentException
*/
public static function hashAndEncrypt(string $password, Key $aesKey): string
{
/** @var string $hash */
$hash = \password_hash(
Base64::encode(
\hash('sha384', $password, true)
),
PASSWORD_DEFAULT
);
if (!\is_string($hash)) {
throw new \Exception("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 \Exception
* @throws \InvalidArgumentException
*/
public static function decryptAndVerifyLegacy(string $password, string $ciphertext, string $aesKey): bool
{
if (Binary::safeStrlen($aesKey) !== 16) {
throw new \Exception("Encryption keys must be 16 bytes long");
}
$hash = Crypto::legacyDecrypt(
$ciphertext,
$aesKey
);
if (!\is_string($hash)) {
throw new \Exception("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 \Exception
* @throws \InvalidArgumentException
*/
public static function decryptAndVerify(string $password, string $ciphertext, Key $aesKey): bool
{
$hash = Crypto::decrypt(
$ciphertext,
$aesKey
);
if (!\is_string($hash)) {
throw new \Exception("Unknown hashing error.");
}
return \password_verify(
Base64::encode(
\hash('sha384', $password, true)
),
$hash
);
}
/**
* 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
*/
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);
}
}
|