118 lines
3.3 KiB
PHP
118 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payment\Support;
|
|
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* ابزار امضای HMAC مطابق EXTERNAL_PAYMENT_PARTNER.md (Montify).
|
|
* فقط سمت سرور استفاده شود.
|
|
*/
|
|
class MontifyHmacSigner
|
|
{
|
|
public function __construct(private string $clientId, private string $clientSecret)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $body
|
|
* @return array<string, string>
|
|
*/
|
|
public function signRequest(string $method, string $path, ?array $body): array
|
|
{
|
|
$timestamp = (string) time();
|
|
$nonce = (string) Str::uuid();
|
|
$bodyString = $body !== null ? $this->stableStringify($body) : '';
|
|
$bodyHash = hash('sha256', $bodyString);
|
|
$canonical = implode("\n", [
|
|
strtoupper($method),
|
|
$path,
|
|
$bodyHash,
|
|
$timestamp,
|
|
$nonce,
|
|
]);
|
|
$signature = hash_hmac('sha256', $canonical, $this->clientSecret);
|
|
|
|
return [
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
'X-Client-Id' => $this->clientId,
|
|
'X-Timestamp' => $timestamp,
|
|
'X-Nonce' => $nonce,
|
|
'X-Signature' => $signature,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $query
|
|
*/
|
|
public function verifyReturnSignature(array $query): bool
|
|
{
|
|
$signature = (string) ($query['signature'] ?? '');
|
|
if ($signature === '') {
|
|
return false;
|
|
}
|
|
|
|
unset($query['signature']);
|
|
$expected = hash_hmac('sha256', $this->stableStringify($query), $this->clientSecret);
|
|
|
|
return $this->hashEqualsHex($signature, $expected);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $body
|
|
*/
|
|
public function verifyWebhookSignature(array $body, ?string $signatureHeader): bool
|
|
{
|
|
if ($signatureHeader === null || $signatureHeader === '') {
|
|
return false;
|
|
}
|
|
|
|
$expected = hash_hmac('sha256', $this->stableStringify($body), $this->clientSecret);
|
|
|
|
return $this->hashEqualsHex($signatureHeader, $expected);
|
|
}
|
|
|
|
public function isTimestampFresh(int|string|null $timestamp, int $skewSeconds = 300): bool
|
|
{
|
|
if ($timestamp === null || $timestamp === '') {
|
|
return false;
|
|
}
|
|
|
|
return abs(time() - (int) $timestamp) <= $skewSeconds;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $value
|
|
*/
|
|
public function stableStringify($value): string
|
|
{
|
|
if ($value === null || !is_array($value)) {
|
|
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
if (array_is_list($value)) {
|
|
$parts = array_map(fn ($item) => $this->stableStringify($item), $value);
|
|
|
|
return '[' . implode(',', $parts) . ']';
|
|
}
|
|
|
|
ksort($value);
|
|
$parts = [];
|
|
foreach ($value as $key => $item) {
|
|
$parts[] = '"' . $key . '":' . $this->stableStringify($item);
|
|
}
|
|
|
|
return '{' . implode(',', $parts) . '}';
|
|
}
|
|
|
|
private function hashEqualsHex(string $a, string $b): bool
|
|
{
|
|
if (!ctype_xdigit($a) || !ctype_xdigit($b) || strlen($a) !== strlen($b)) {
|
|
return false;
|
|
}
|
|
|
|
return hash_equals(strtolower($a), strtolower($b));
|
|
}
|
|
}
|