wip: encriptacion
This commit is contained in:
parent
aac383fc83
commit
50e028827f
182
app/Helpers/EncryptionHelper.php
Normal file
182
app/Helpers/EncryptionHelper.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EncryptionHelper
|
||||
{
|
||||
/**
|
||||
* Encrypt the given data (arrays/objects).
|
||||
*/
|
||||
public static function encryptData($data)
|
||||
{
|
||||
try {
|
||||
return Crypt::encryptString(json_encode($data));
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException("Error al encriptar los datos: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the given data (arrays/objects).
|
||||
*/
|
||||
public static function decryptData($encryptedData)
|
||||
{
|
||||
try {
|
||||
$decrypted = Crypt::decryptString($encryptedData);
|
||||
return json_decode($decrypted, true);
|
||||
} catch (DecryptException $e) {
|
||||
Log::error('Error al desencriptar los datos: ' . $e->getMessage());
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error inesperado al desencriptar los datos: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a simple string (for tokens, passwords, etc.)
|
||||
*/
|
||||
public static function encryptString(string $string): string
|
||||
{
|
||||
try {
|
||||
return Crypt::encryptString($string);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException("Error al encriptar el string: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a simple string
|
||||
*/
|
||||
public static function decryptString(string $encryptedString): ?string
|
||||
{
|
||||
try {
|
||||
return Crypt::decryptString($encryptedString);
|
||||
} catch (DecryptException $e) {
|
||||
Log::error('Error al desencriptar el string: ' . $e->getMessage());
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error inesperado al desencriptar el string: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt using a custom key (independent of APP_KEY)
|
||||
* Useful for tokens that should survive APP_KEY rotation
|
||||
*/
|
||||
public static function encryptWithCustomKey(string $string, string $key): string
|
||||
{
|
||||
try {
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivLength = openssl_cipher_iv_length($cipher);
|
||||
$iv = openssl_random_pseudo_bytes($ivLength);
|
||||
|
||||
$encrypted = openssl_encrypt($string, $cipher, $key, 0, $iv);
|
||||
|
||||
if ($encrypted === false) {
|
||||
throw new \RuntimeException('Encryption failed');
|
||||
}
|
||||
|
||||
// Combinar IV + encrypted data y codificar en base64
|
||||
return base64_encode($iv . $encrypted);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException("Error al encriptar con clave personalizada: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt using a custom key (independent of APP_KEY)
|
||||
*/
|
||||
public static function decryptWithCustomKey(string $encryptedString, string $key): ?string
|
||||
{
|
||||
try {
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivLength = openssl_cipher_iv_length($cipher);
|
||||
|
||||
// Decodificar y separar IV + encrypted data
|
||||
$data = base64_decode($encryptedString);
|
||||
if ($data === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$iv = substr($data, 0, $ivLength);
|
||||
$encrypted = substr($data, $ivLength);
|
||||
|
||||
$decrypted = openssl_decrypt($encrypted, $cipher, $key, 0, $iv);
|
||||
|
||||
if ($decrypted === false) {
|
||||
Log::error('Error al desencriptar con clave personalizada');
|
||||
return null;
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error inesperado al desencriptar con clave personalizada: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a plain value matches a value encrypted with custom key
|
||||
*/
|
||||
public static function verifyWithCustomKey(string $plainValue, string $encryptedValue, string $key): bool
|
||||
{
|
||||
try {
|
||||
$decrypted = self::decryptWithCustomKey($encryptedValue, $key);
|
||||
return $decrypted === $plainValue;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash for searchable encrypted data
|
||||
*/
|
||||
public static function hash(string $data, string $algorithm = 'sha256'): string
|
||||
{
|
||||
return hash($algorithm, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt multiple fields in an array
|
||||
*/
|
||||
public static function encryptFields(array $data, array $fields): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$data[$field] = self::encryptData($data[$field]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt multiple fields in an array
|
||||
*/
|
||||
public static function decryptFields(array $data, array $fields): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$data[$field] = self::decryptData($data[$field]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a plain value matches an encrypted value
|
||||
*/
|
||||
public static function verifyEncrypted(string $plainValue, string $encryptedValue): bool
|
||||
{
|
||||
try {
|
||||
$decrypted = self::decryptString($encryptedValue);
|
||||
return $decrypted === $plainValue;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,8 +32,13 @@ public function index(Request $request)
|
||||
|
||||
$arcos = $query->orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Agregar token desencriptado a cada arco
|
||||
$arcos->each(function($arco) {
|
||||
$arco->api_token_plain = $arco->obtenerTokenDesencriptado();
|
||||
});
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'arcos' => $arcos
|
||||
'arcos' => $arcos,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -48,15 +53,22 @@ public function store(Request $request)
|
||||
'ip_address' => 'required|ip|unique:arcos,ip_address',
|
||||
'ubicacion' => 'nullable|string|max:255',
|
||||
'descripcion' => 'nullable|string',
|
||||
'antena_1' => 'nullable|string',
|
||||
'antena_2' => 'nullable|string',
|
||||
'antena_3' => 'nullable|string',
|
||||
'antena_4' => 'nullable|string',
|
||||
'activo' => 'boolean'
|
||||
]);
|
||||
|
||||
$arco = Arco::create($validated);
|
||||
|
||||
// Obtener el token en texto plano para mostrárselo al usuario
|
||||
$plainToken = $arco->obtenerTokenDesencriptado();
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Arco creado exitosamente',
|
||||
'arco' => $arco,
|
||||
'api_token' => $arco->api_token
|
||||
'api_token' => $plainToken // Token en texto plano para que el arco lo use
|
||||
]);
|
||||
}
|
||||
|
||||
@ -100,7 +112,10 @@ public function update(Request $request, int $id)
|
||||
'ip_address' => 'sometimes|required|ip|unique:arcos,ip_address,' . $id,
|
||||
'ubicacion' => 'nullable|string|max:255',
|
||||
'descripcion' => 'nullable|string',
|
||||
'activo' => 'boolean'
|
||||
'antena_1' => 'nullable|string',
|
||||
'antena_2' => 'nullable|string',
|
||||
'antena_3' => 'nullable|string',
|
||||
'antena_4' => 'nullable|string'
|
||||
]);
|
||||
|
||||
$arco->update($validated);
|
||||
|
||||
@ -15,15 +15,16 @@ class ArcoTokenMiddleware
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = $request->bearerToken();
|
||||
$tokenPlano = $request->bearerToken();
|
||||
|
||||
if (!$token) {
|
||||
if (!$tokenPlano) {
|
||||
return ApiResponse::UNAUTHORIZED->response([
|
||||
'message' => 'Token de arco no proporcionado'
|
||||
]);
|
||||
}
|
||||
|
||||
$arco = Arco::buscarPorToken($token);
|
||||
// Buscar arco por token plano (el método desencripta internamente)
|
||||
$arco = Arco::buscarPorToken($tokenPlano);
|
||||
|
||||
if (!$arco) {
|
||||
return ApiResponse::UNAUTHORIZED->response([
|
||||
@ -37,6 +38,20 @@ public function handle(Request $request, Closure $next): Response
|
||||
]);
|
||||
}
|
||||
|
||||
// Validación de IP deshabilitada para desarrollo
|
||||
// TODO: Habilitar en producción o configurar nginx para enviar X-Forwarded-For
|
||||
// $requestIp = $request->ip();
|
||||
// if ($arco->ip_address !== $requestIp) {
|
||||
// return ApiResponse::FORBIDDEN->response([
|
||||
// 'message' => 'Token no autorizado para esta IP',
|
||||
// 'detail' => [
|
||||
// 'ip_registrada_arco' => $arco->ip_address,
|
||||
// 'ip_del_request' => $requestIp,
|
||||
// 'arco_nombre' => $arco->nombre
|
||||
// ]
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// Agregar el arco al request para uso posterior
|
||||
$request->merge(['arco_autenticado' => $arco]);
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use App\Helpers\EncryptionHelper;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
@ -25,7 +25,11 @@ class Arco extends Model
|
||||
'nombre',
|
||||
'ip_address',
|
||||
'ubicacion',
|
||||
'activo'
|
||||
'activo',
|
||||
'antena_1',
|
||||
'antena_2',
|
||||
'antena_3',
|
||||
'antena_4'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@ -44,7 +48,8 @@ protected static function boot()
|
||||
|
||||
static::creating(function ($arco) {
|
||||
if (!$arco->api_token) {
|
||||
$arco->api_token = Str::random(64);
|
||||
$plainToken = Str::random(64);
|
||||
$arco->api_token = EncryptionHelper::encryptString($plainToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -74,20 +79,39 @@ public static function buscarPorIp(string $ip): ?self
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar arco por API token
|
||||
* Buscar arco por API token (recibe token plano, busca desencriptando)
|
||||
*/
|
||||
public static function buscarPorToken(string $token): ?self
|
||||
public static function buscarPorToken(string $tokenPlano): ?self
|
||||
{
|
||||
return self::where('api_token', $token)->first();
|
||||
$arcos = self::all();
|
||||
|
||||
foreach ($arcos as $arco) {
|
||||
// Desencriptar el token de la BD y comparar con el token plano recibido
|
||||
if (EncryptionHelper::verifyEncrypted($tokenPlano, $arco->api_token)) {
|
||||
return $arco;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerar API token
|
||||
* Regenerar token encriptado
|
||||
*/
|
||||
public function regenerarToken(): string
|
||||
{
|
||||
$this->api_token = Str::random(64);
|
||||
$plainToken = Str::random(64);
|
||||
$this->api_token = EncryptionHelper::encryptString($plainToken);
|
||||
$this->save();
|
||||
return $this->api_token;
|
||||
|
||||
return $plainToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener token desencriptado
|
||||
*/
|
||||
public function obtenerTokenDesencriptado(): ?string
|
||||
{
|
||||
return EncryptionHelper::decryptString($this->api_token);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ class Detection extends Model
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'arco_id',
|
||||
'epc',
|
||||
'vin',
|
||||
'placa',
|
||||
@ -29,4 +30,12 @@ class Detection extends Model
|
||||
protected $casts = [
|
||||
'fecha_deteccion' => 'datetime:Y-m-d H:i:s'
|
||||
];
|
||||
|
||||
/**
|
||||
* Relación con el modelo Arco
|
||||
*/
|
||||
public function arco()
|
||||
{
|
||||
return $this->belongsTo(Arco::class, 'arco_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,14 +45,14 @@ private function consultarNuevoVehiculo(string $epc, string $fastId): array
|
||||
$datosVehiculo = $this->consultaRepuveCons->consultarVehiculoPorTag($fastId);
|
||||
|
||||
if (!$datosVehiculo || !$datosVehiculo['vin']) {
|
||||
Log::warning('Vehículo NO encontrado en API externa', [
|
||||
Log::warning('Vehículo NO encontrado.', [
|
||||
'epc' => $epc,
|
||||
'fast_id' => $fastId
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No se encontró información del vehículo en la API externa'
|
||||
'message' => 'No se encontró información del vehículo.'
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -38,6 +38,15 @@
|
||||
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
|
||||
'arco.token' => \App\Http\Middleware\ArcoTokenMiddleware::class,
|
||||
]);
|
||||
|
||||
// Configurar proxies confiables para detectar IP real en Docker
|
||||
$middleware->trustProxies(
|
||||
at: '*',
|
||||
headers: Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO
|
||||
);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
$exceptions->render(function (ServiceUnavailableHttpException $e, Request $request) {
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$indexes = DB::select("SHOW INDEX FROM arcos WHERE Column_name = 'api_token'");
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
$indexName = $index->Key_name;
|
||||
try {
|
||||
DB::statement("ALTER TABLE arcos DROP INDEX `{$indexName}`");
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
DB::statement('ALTER TABLE arcos MODIFY COLUMN api_token TEXT NULL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement('ALTER TABLE arcos MODIFY COLUMN api_token VARCHAR(64) NULL');
|
||||
|
||||
Schema::table('arcos', function (Blueprint $table) {
|
||||
$table->unique('api_token');
|
||||
$table->index('api_token');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('arcos', function (Blueprint $table) {
|
||||
// Agregar 4 campos para antenas (todas opcionales/nullable)
|
||||
$table->string('antena_1')->nullable()->after('activo');
|
||||
$table->string('antena_2')->nullable()->after('antena_1');
|
||||
$table->string('antena_3')->nullable()->after('antena_2');
|
||||
$table->string('antena_4')->nullable()->after('antena_3');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('arcos', function (Blueprint $table) {
|
||||
$table->dropColumn(['antena_1', 'antena_2', 'antena_3', 'antena_4']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user