Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
324
src/Framework/Cryptography/KeyDerivationFunction.php
Normal file
324
src/Framework/Cryptography/KeyDerivationFunction.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cryptography;
|
||||
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Key Derivation Function Service
|
||||
*
|
||||
* Provides secure key derivation functions for password hashing and key stretching.
|
||||
* Supports PBKDF2, Argon2ID, and scrypt algorithms.
|
||||
*/
|
||||
final readonly class KeyDerivationFunction
|
||||
{
|
||||
public function __construct(
|
||||
private RandomGenerator $randomGenerator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate salt for key derivation
|
||||
*/
|
||||
public function generateSalt(int $length = 32): string
|
||||
{
|
||||
if ($length < 16) {
|
||||
throw new InvalidArgumentException('Salt length must be at least 16 bytes');
|
||||
}
|
||||
|
||||
if ($length > 256) {
|
||||
throw new InvalidArgumentException('Salt length cannot exceed 256 bytes');
|
||||
}
|
||||
|
||||
return $this->randomGenerator->bytes($length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive key using PBKDF2
|
||||
*/
|
||||
public function pbkdf2(
|
||||
string $password,
|
||||
string $salt,
|
||||
int $iterations = 100000,
|
||||
int $keyLength = 32,
|
||||
string $algorithm = 'sha256'
|
||||
): DerivedKey {
|
||||
if (empty($password)) {
|
||||
throw new InvalidArgumentException('Password cannot be empty');
|
||||
}
|
||||
|
||||
if (strlen($salt) < 16) {
|
||||
throw new InvalidArgumentException('Salt must be at least 16 bytes');
|
||||
}
|
||||
|
||||
if ($iterations < 10000) {
|
||||
throw new InvalidArgumentException('Iterations must be at least 10,000 for security');
|
||||
}
|
||||
|
||||
if ($keyLength < 16 || $keyLength > 256) {
|
||||
throw new InvalidArgumentException('Key length must be between 16 and 256 bytes');
|
||||
}
|
||||
|
||||
$supportedAlgorithms = ['sha256', 'sha512', 'sha384', 'sha224'];
|
||||
if (! in_array($algorithm, $supportedAlgorithms, true)) {
|
||||
throw new InvalidArgumentException('Unsupported hash algorithm');
|
||||
}
|
||||
|
||||
$derivedKey = hash_pbkdf2($algorithm, $password, $salt, $iterations, $keyLength, true);
|
||||
|
||||
return new DerivedKey(
|
||||
key: $derivedKey,
|
||||
salt: $salt,
|
||||
algorithm: "pbkdf2-{$algorithm}",
|
||||
iterations: $iterations,
|
||||
keyLength: $keyLength
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive key using Argon2ID (recommended)
|
||||
*/
|
||||
public function argon2id(
|
||||
string $password,
|
||||
string $salt,
|
||||
int $memoryCost = 65536, // 64 MB
|
||||
int $timeCost = 4,
|
||||
int $threads = 3,
|
||||
int $keyLength = 32
|
||||
): DerivedKey {
|
||||
if (empty($password)) {
|
||||
throw new InvalidArgumentException('Password cannot be empty');
|
||||
}
|
||||
|
||||
if (strlen($salt) < 16) {
|
||||
throw new InvalidArgumentException('Salt must be at least 16 bytes');
|
||||
}
|
||||
|
||||
if ($memoryCost < 1024) {
|
||||
throw new InvalidArgumentException('Memory cost must be at least 1024 KB');
|
||||
}
|
||||
|
||||
if ($timeCost < 1) {
|
||||
throw new InvalidArgumentException('Time cost must be at least 1');
|
||||
}
|
||||
|
||||
if ($threads < 1 || $threads > 24) {
|
||||
throw new InvalidArgumentException('Threads must be between 1 and 24');
|
||||
}
|
||||
|
||||
if ($keyLength < 16 || $keyLength > 256) {
|
||||
throw new InvalidArgumentException('Key length must be between 16 and 256 bytes');
|
||||
}
|
||||
|
||||
if (! function_exists('sodium_crypto_pwhash')) {
|
||||
throw new InvalidArgumentException('Sodium extension required for Argon2ID');
|
||||
}
|
||||
|
||||
if (! defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID')) {
|
||||
throw new InvalidArgumentException('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID constant not available');
|
||||
}
|
||||
|
||||
$derivedKey = sodium_crypto_pwhash(
|
||||
$keyLength,
|
||||
$password,
|
||||
$salt,
|
||||
$timeCost,
|
||||
$memoryCost,
|
||||
SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID
|
||||
);
|
||||
|
||||
return new DerivedKey(
|
||||
key: $derivedKey,
|
||||
salt: $salt,
|
||||
algorithm: 'argon2id',
|
||||
iterations: $timeCost,
|
||||
keyLength: $keyLength,
|
||||
memoryCost: $memoryCost,
|
||||
threads: $threads
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive key using scrypt
|
||||
*/
|
||||
public function scrypt(
|
||||
string $password,
|
||||
string $salt,
|
||||
int $costParameter = 16384, // N
|
||||
int $blockSize = 8, // r
|
||||
int $parallelization = 1, // p
|
||||
int $keyLength = 32
|
||||
): DerivedKey {
|
||||
if (empty($password)) {
|
||||
throw new InvalidArgumentException('Password cannot be empty');
|
||||
}
|
||||
|
||||
if (strlen($salt) < 16) {
|
||||
throw new InvalidArgumentException('Salt must be at least 16 bytes');
|
||||
}
|
||||
|
||||
if ($costParameter < 1024) {
|
||||
throw new InvalidArgumentException('Cost parameter must be at least 1024');
|
||||
}
|
||||
|
||||
if ($blockSize < 1) {
|
||||
throw new InvalidArgumentException('Block size must be at least 1');
|
||||
}
|
||||
|
||||
if ($parallelization < 1) {
|
||||
throw new InvalidArgumentException('Parallelization must be at least 1');
|
||||
}
|
||||
|
||||
if ($keyLength < 16 || $keyLength > 256) {
|
||||
throw new InvalidArgumentException('Key length must be between 16 and 256 bytes');
|
||||
}
|
||||
|
||||
if (! function_exists('scrypt')) {
|
||||
throw new InvalidArgumentException('scrypt function not available');
|
||||
}
|
||||
|
||||
$derivedKey = scrypt(
|
||||
$password,
|
||||
$salt,
|
||||
$costParameter,
|
||||
$blockSize,
|
||||
$parallelization,
|
||||
$keyLength
|
||||
);
|
||||
|
||||
return new DerivedKey(
|
||||
key: $derivedKey,
|
||||
salt: $salt,
|
||||
algorithm: 'scrypt',
|
||||
iterations: $costParameter,
|
||||
keyLength: $keyLength,
|
||||
blockSize: $blockSize,
|
||||
parallelization: $parallelization
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password against derived key
|
||||
*/
|
||||
public function verify(string $password, DerivedKey $derivedKey): bool
|
||||
{
|
||||
try {
|
||||
$verificationKey = match ($derivedKey->getAlgorithm()) {
|
||||
'pbkdf2-sha256' => $this->pbkdf2(
|
||||
$password,
|
||||
$derivedKey->getSalt(),
|
||||
$derivedKey->getIterations(),
|
||||
$derivedKey->getKeyLength(),
|
||||
'sha256'
|
||||
),
|
||||
'pbkdf2-sha512' => $this->pbkdf2(
|
||||
$password,
|
||||
$derivedKey->getSalt(),
|
||||
$derivedKey->getIterations(),
|
||||
$derivedKey->getKeyLength(),
|
||||
'sha512'
|
||||
),
|
||||
'argon2id' => $this->argon2id(
|
||||
$password,
|
||||
$derivedKey->getSalt(),
|
||||
$derivedKey->getMemoryCost() ?? 65536,
|
||||
$derivedKey->getIterations(),
|
||||
$derivedKey->getThreads() ?? 3,
|
||||
$derivedKey->getKeyLength()
|
||||
),
|
||||
'scrypt' => $this->scrypt(
|
||||
$password,
|
||||
$derivedKey->getSalt(),
|
||||
$derivedKey->getIterations(),
|
||||
$derivedKey->getBlockSize() ?? 8,
|
||||
$derivedKey->getParallelization() ?? 1,
|
||||
$derivedKey->getKeyLength()
|
||||
),
|
||||
default => throw new InvalidArgumentException('Unsupported algorithm: ' . $derivedKey->getAlgorithm())
|
||||
};
|
||||
|
||||
return hash_equals($derivedKey->getKey(), $verificationKey->getKey());
|
||||
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash password with automatic salt generation (recommended for new passwords)
|
||||
*/
|
||||
public function hashPassword(
|
||||
string $password,
|
||||
string $algorithm = 'argon2id',
|
||||
array $options = []
|
||||
): DerivedKey {
|
||||
$salt = $this->generateSalt(32);
|
||||
|
||||
return match ($algorithm) {
|
||||
'pbkdf2-sha256' => $this->pbkdf2(
|
||||
$password,
|
||||
$salt,
|
||||
$options['iterations'] ?? 100000,
|
||||
$options['key_length'] ?? 32,
|
||||
'sha256'
|
||||
),
|
||||
'pbkdf2-sha512' => $this->pbkdf2(
|
||||
$password,
|
||||
$salt,
|
||||
$options['iterations'] ?? 100000,
|
||||
$options['key_length'] ?? 32,
|
||||
'sha512'
|
||||
),
|
||||
'argon2id' => $this->argon2id(
|
||||
$password,
|
||||
$salt,
|
||||
$options['memory_cost'] ?? 65536,
|
||||
$options['time_cost'] ?? 4,
|
||||
$options['threads'] ?? 3,
|
||||
$options['key_length'] ?? 32
|
||||
),
|
||||
'scrypt' => $this->scrypt(
|
||||
$password,
|
||||
$salt,
|
||||
$options['cost_parameter'] ?? 16384,
|
||||
$options['block_size'] ?? 8,
|
||||
$options['parallelization'] ?? 1,
|
||||
$options['key_length'] ?? 32
|
||||
),
|
||||
default => throw new InvalidArgumentException("Unsupported algorithm: {$algorithm}")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended parameters for different security levels
|
||||
*/
|
||||
public function getRecommendedParameters(string $algorithm, string $securityLevel = 'standard'): array
|
||||
{
|
||||
return match ([$algorithm, $securityLevel]) {
|
||||
['pbkdf2-sha256', 'low'] => ['iterations' => 50000, 'key_length' => 32],
|
||||
['pbkdf2-sha256', 'standard'] => ['iterations' => 100000, 'key_length' => 32],
|
||||
['pbkdf2-sha256', 'high'] => ['iterations' => 200000, 'key_length' => 32],
|
||||
|
||||
['argon2id', 'low'] => ['memory_cost' => 32768, 'time_cost' => 3, 'threads' => 2, 'key_length' => 32],
|
||||
['argon2id', 'standard'] => ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3, 'key_length' => 32],
|
||||
['argon2id', 'high'] => ['memory_cost' => 131072, 'time_cost' => 6, 'threads' => 4, 'key_length' => 32],
|
||||
|
||||
['scrypt', 'low'] => ['cost_parameter' => 8192, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32],
|
||||
['scrypt', 'standard'] => ['cost_parameter' => 16384, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32],
|
||||
['scrypt', 'high'] => ['cost_parameter' => 32768, 'block_size' => 8, 'parallelization' => 2, 'key_length' => 32],
|
||||
|
||||
default => throw new InvalidArgumentException("Unsupported algorithm or security level: {$algorithm}, {$securityLevel}")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method
|
||||
*/
|
||||
public static function create(RandomGenerator $randomGenerator): self
|
||||
{
|
||||
return new self($randomGenerator);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user