PHP 8.4+ Framework
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

127 lines
4.4 KiB

<?php
declare(strict_types=1);
namespace CodeHydrater\services\paragon_crypto;
/**
* @copyright Paragon Initiative Enterprises
* @license Creative Commons Attribution 4.0 International / ISC
* @link https://github.com/paragonie/pecl-libsodium-doc/blob/master/chapters/09-recipes.md
*/
class password_storage {
const SALT_SIZE_IN_BYTES = 16;
private $random_engine;
public function __construct() {
$this->random_engine = new \tts\random_engine();
}
public function generate_a_key(): string {
return sodium_bin2hex($this->random_engine->get_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES));
}
/**
* Hash then encrypt a password
*
* @param string $password - The user's password
* @param string $secret_key - The main_magic key for all passwords
* @return string
*/
public function hash(#[\SensitiveParameter] string $password, #[\SensitiveParameter] string $secret_key): string {
// First, let's calculate the hash
$hashed = sodium_crypto_pwhash_scryptsalsa208sha256_str(
$password,
SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_MEMLIMIT_INTERACTIVE
);
$salt = $this->random_engine->get_bytes(self::SALT_SIZE_IN_BYTES);
list ($enc_key, $auth_key) = $this->split_keys($secret_key, sodium_bin2hex($salt));
sodium_memzero($secret_key);
sodium_memzero($password);
$nonce = $this->random_engine->get_bytes(
SODIUM_CRYPTO_STREAM_NONCEBYTES
);
$cipher_text = sodium_crypto_stream_xor(
$hashed,
$nonce,
$enc_key
);
$mac = sodium_crypto_auth($nonce . $cipher_text, $auth_key);
sodium_memzero($enc_key);
sodium_memzero($auth_key);
return sodium_bin2hex($mac . $nonce . $salt . $cipher_text);
}
/**
* Decrypt then verify a password
*
* @param string $password - The user-provided password
* @param string $stored - The encrypted password hash
* @param string $secret_key - The main_magic key for all passwords
*/
public function verify(#[\SensitiveParameter] string $password, string $stored, #[\SensitiveParameter] string $secret_key): bool {
$data = sodium_hex2bin($stored);
$mac = mb_substr(
$data,
0,
SODIUM_CRYPTO_AUTH_BYTES,
'8bit'
);
$nonce = mb_substr(
$data,
SODIUM_CRYPTO_AUTH_BYTES,
SODIUM_CRYPTO_STREAM_NONCEBYTES,
'8bit'
);
$salt = mb_substr(
$data,
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES,
self::SALT_SIZE_IN_BYTES,
'8bit'
);
$cipher_text = mb_substr(
$data,
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES + self::SALT_SIZE_IN_BYTES,
null,
'8bit'
);
list ($enc_key, $auth_key) = $this->split_keys($secret_key, sodium_bin2hex($salt));
if (sodium_crypto_auth_verify($mac, $nonce . $cipher_text, $auth_key)) {
sodium_memzero($auth_key);
$hash_str = sodium_crypto_stream_xor($cipher_text, $nonce, $enc_key);
sodium_memzero($enc_key);
if ($hash_str !== false) {
$ret = sodium_crypto_pwhash_scryptsalsa208sha256_str_verify($hash_str, $password);
sodium_memzero($password);
return $ret;
}
} else {
sodium_memzero($auth_key);
sodium_memzero($enc_key);
}
throw new \Exception('Decryption failed.');
}
/**
* @return array(2) [encryption key, authentication key]
*/
private function split_keys(#[\SensitiveParameter] string $secret_key, #[\SensitiveParameter] string $salt): array {
$enc_key = hash_hkdf('sha256', $secret_key, SODIUM_CRYPTO_STREAM_KEYBYTES, 'encryption', $salt);
$auth_key = hash_hkdf('sha256', $secret_key, SODIUM_CRYPTO_AUTH_KEYBYTES, 'authentication', $salt);
return [$enc_key, $auth_key];
}
}
/*
$c = new \tts\services\paragon_crypto\password_storage();
$k = $c->generate_a_key();
$h = $c->hash("HelpMe", $k);
var_dump( $c->verify("HelpMe", $h, $k) );
*/