75 lines
2.3 KiB
PHP
75 lines
2.3 KiB
PHP
<?php
|
||
|
||
namespace App\Models\Concerns;
|
||
|
||
use App\Services\Crypto\PdnCipher;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Throwable;
|
||
|
||
/**
|
||
* Прозрачное шифрование полей персональных данных на уровне приложения.
|
||
*
|
||
* Мера ФСТЭК: ЗНИ (защита носителей), ОЦЛ.2 (контроль целостности данных).
|
||
*
|
||
* Использование в модели:
|
||
*
|
||
* use HasPdnEncryption;
|
||
* protected array $encrypted = ['last_name', 'passport', 'snils'];
|
||
*
|
||
* Шифрование выполняется выбранным драйвером (config/security.php → encryption),
|
||
* что позволяет заменить AES на сертифицированное СКЗИ (ГОСТ) без правки моделей.
|
||
*/
|
||
trait HasPdnEncryption
|
||
{
|
||
public function setAttribute($key, $value)
|
||
{
|
||
if ($this->isPdnEncrypted($key) && $value !== null) {
|
||
$value = $this->pdnCipher()->encrypt((string) $value);
|
||
}
|
||
|
||
return parent::setAttribute($key, $value);
|
||
}
|
||
|
||
public function getAttribute($key)
|
||
{
|
||
$value = parent::getAttribute($key);
|
||
|
||
if ($this->isPdnEncrypted($key) && $value !== null && is_string($value)) {
|
||
try {
|
||
return $this->pdnCipher()->decrypt($value);
|
||
} catch (Throwable $e) {
|
||
// Нарушение целостности/невозможность расшифровки — инцидент (ИНЦ.1).
|
||
Log::channel(config('audit.siem.channel', 'stack'))->error('pdn.decrypt_failed', [
|
||
'model' => static::class,
|
||
'attribute' => $key,
|
||
'id' => $this->getKey(),
|
||
]);
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Список зашифрованных атрибутов модели.
|
||
*
|
||
* @return array<int, string>
|
||
*/
|
||
public function encryptedAttributes(): array
|
||
{
|
||
return $this->encrypted;
|
||
}
|
||
|
||
protected function isPdnEncrypted(string $key): bool
|
||
{
|
||
return in_array($key, $this->encryptedAttributes(), true);
|
||
}
|
||
|
||
protected function pdnCipher(): PdnCipher
|
||
{
|
||
return app(PdnCipher::class);
|
||
}
|
||
}
|