51 lines
1.7 KiB
PHP
51 lines
1.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Security;
|
|
|
|
use App\Core\Redis;
|
|
|
|
final class HmacService
|
|
{
|
|
/**
|
|
* Verify HMAC signature for external API requests (Flutter)
|
|
*/
|
|
public function verify(string $secret, string $method, string $path,
|
|
string $timestamp, string $nonce, string $body, string $signature): bool
|
|
{
|
|
// 1. Timestamp window (±5 minutes)
|
|
if (abs(time() - (int)$timestamp) > 300) return false;
|
|
|
|
// 2. Nonce replay protection
|
|
try {
|
|
$redis = \App\Core\Redis::getInstance();
|
|
$nonceKey = 'hmac_nonce:' . $nonce;
|
|
if ($redis->exists($nonceKey)) return false; // Replay attack
|
|
$redis->setex($nonceKey, 600, '1'); // TTL 10 minutes
|
|
} catch (\Throwable $e) {
|
|
// Redis unavailable — log but don't fail (degrade gracefully)
|
|
error_log('[HMAC] Redis unavailable for nonce check: ' . $e->getMessage());
|
|
}
|
|
|
|
// 3. Build & compare signature
|
|
$bodyHash = hash('sha256', $body);
|
|
$stringToSign = strtoupper($method) . "\n" . $path . "\n" . $timestamp . "\n" . $nonce . "\n" . $bodyHash;
|
|
$calculated = hash_hmac('sha256', $stringToSign, $secret);
|
|
|
|
return hash_equals($calculated, $signature);
|
|
}
|
|
|
|
public function sign(string $secret, string $method, string $path, string $timestamp, string $nonce, string $body): string
|
|
{
|
|
$bodyHash = hash('sha256', $body);
|
|
$stringToSign = strtoupper($method) . "\n" .
|
|
$path . "\n" .
|
|
$timestamp . "\n" .
|
|
$nonce . "\n" .
|
|
$bodyHash;
|
|
|
|
return hash_hmac('sha256', $stringToSign, $secret);
|
|
}
|
|
}
|