feat(security): argon2id password hashing and AES for SignRSA secrets

- Hash driver read from HASH_DRIVER (argon2id in production); bcrypt
  hashes still verify and are rehashed on successful admin/client login
  since the JWT guard and guard->validate() never rehash on their own.
- Client registration uses Hash::make instead of bcrypt(), which always
  forced bcrypt regardless of the configured driver.
- SignRSA now encrypts with Crypt (AES-256-CBC + HMAC, APP_KEY) instead
  of 3DES in ECB mode without IV. Legacy 3DES values are still readable
  via RSA_KEY/RSA_PUBLIC until `secrets:reencrypt` migrates them;
  RSA_PRIVATE is no longer used.
- LegacyPortalImporter stored module_vmware.password in plaintext; it is
  now encrypted, and a migration encrypts existing plaintext rows.

Deploy: php artisan migrate, then secrets:reencrypt --dry-run and
secrets:reencrypt; remove RSA_* from env once it exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-15 19:56:38 +07:00
co-authored by Claude Opus 5.5
parent 0bfb3bb49c
commit ea5697cb81
11 changed files with 166 additions and 69 deletions
+31 -44
View File
@@ -2,64 +2,51 @@
namespace App\Http\Services;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
class SignRSA
{
protected string $key;
protected ?string $private;
protected string $public;
protected ?string $previousKey;
protected ?string $previousPublic;
private const LEGACY_KEYS = [['rsa.key', 'rsa.public'], ['rsa.previous_key', 'rsa.previous_public']];
public function __construct()
public function EncryptFunc($plaintext): string
{
$this->key = base64_decode(config('rsa.key'));
$this->private = str_replace('\n', "\n", config('rsa.private'));
$this->public = str_replace('\n', "\n", config('rsa.public'));
$this->previousKey = config('rsa.previous_key') ? base64_decode(config('rsa.previous_key')) : null;
$this->previousPublic = config('rsa.previous_public') ? str_replace('\n', "\n", config('rsa.previous_public')) : null;
return Crypt::encryptString((string) $plaintext);
}
public function EncryptFunc($signdata): string
{
$sign = $this->SignData($signdata);
return $this->EncryptedData($signdata . '|' . $sign, $this->key);
}
private function SignData($signdata = ''): string
{
openssl_sign($signdata, $result, $this->private, 'sha256WithRSAEncryption');
return base64_encode($result);
}
private function EncryptedData($original_schema = '', $key_hash = ''): string
{
$encrypted_data = openssl_encrypt($original_schema, 'DES-EDE3', $key_hash, OPENSSL_RAW_DATA);
return base64_encode($encrypted_data);
}
/*
* Function Decrypt Data
*/
public function DecryptFunc($data): string
{
if (!$data) {
return false;
return '';
}
try {
return Crypt::decryptString($data);
} catch (DecryptException) {
return $this->decryptLegacy((string) $data);
}
}
private function decryptLegacy(string $data): string
{
$encrypted_data = base64_decode($data);
$verified = $this->Verify_Sign((string) $this->DecryptData($encrypted_data, $this->key), $this->public);
if ($verified === false && $this->previousKey && $this->previousPublic) {
$verified = $this->Verify_Sign((string) $this->DecryptData($encrypted_data, $this->previousKey), $this->previousPublic);
foreach (self::LEGACY_KEYS as [$keyConfig, $publicConfig]) {
if (!config($keyConfig) || !config($publicConfig)) {
continue;
}
$verified = $this->Verify_Sign(
(string) $this->DecryptData($encrypted_data, base64_decode(config($keyConfig))),
str_replace('\n', "\n", config($publicConfig)),
);
if ($verified !== false) {
return $verified;
}
}
return $verified;
return '';
}
private function DecryptData($encrypted_data, $random_key): bool|string