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) ); */