- New SECRET_ANALYTICS variable replaces kernel.secret for analytics - Ansible generates a random 32-char secret at each deploy - Endpoint token and encryption key change with every deployment - Existing sessions will get new visitor_id after deploy (expected) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.2 KiB
PHP
75 lines
2.2 KiB
PHP
<?php
|
|
|
|
namespace App\Service;
|
|
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
|
|
class AnalyticsCryptoService
|
|
{
|
|
private string $key;
|
|
|
|
public function __construct(
|
|
#[Autowire(env: 'SECRET_ANALYTICS')] private string $analyticsSecret,
|
|
) {
|
|
$this->key = substr(hash('sha256', $this->analyticsSecret, true), 0, 32);
|
|
}
|
|
|
|
public function encrypt(array $data): string
|
|
{
|
|
$json = json_encode($data, \JSON_THROW_ON_ERROR);
|
|
$iv = random_bytes(12);
|
|
$encrypted = openssl_encrypt($json, 'aes-256-gcm', $this->key, \OPENSSL_RAW_DATA, $iv, $tag, '', 16);
|
|
|
|
// Format compatible with Web Crypto API: iv + ciphertext + tag
|
|
return base64_encode($iv.$encrypted.$tag);
|
|
}
|
|
|
|
public function decrypt(string $payload): ?array
|
|
{
|
|
$raw = base64_decode($payload, true);
|
|
if (false === $raw || \strlen($raw) < 28) {
|
|
return null;
|
|
}
|
|
|
|
// Try JS format first: iv (12) + ciphertext_with_tag (tag is last 16 bytes)
|
|
$iv = substr($raw, 0, 12);
|
|
$ciphertextWithTag = substr($raw, 12);
|
|
|
|
if (\strlen($ciphertextWithTag) >= 16) {
|
|
$tag = substr($ciphertextWithTag, -16);
|
|
$encrypted = substr($ciphertextWithTag, 0, -16);
|
|
|
|
$json = openssl_decrypt($encrypted, 'aes-256-gcm', $this->key, \OPENSSL_RAW_DATA, $iv, $tag);
|
|
if (false !== $json) {
|
|
return json_decode($json, true);
|
|
}
|
|
}
|
|
|
|
// Fallback: PHP format iv (12) + tag (16) + ciphertext
|
|
$tag = substr($raw, 12, 16);
|
|
$encrypted = substr($raw, 28);
|
|
|
|
$json = openssl_decrypt($encrypted, 'aes-256-gcm', $this->key, \OPENSSL_RAW_DATA, $iv, $tag);
|
|
if (false === $json) {
|
|
return null;
|
|
}
|
|
|
|
return json_decode($json, true);
|
|
}
|
|
|
|
public function generateVisitorHash(string $uid): string
|
|
{
|
|
return hash_hmac('sha256', $uid, $this->key);
|
|
}
|
|
|
|
public function verifyVisitorHash(string $uid, string $hash): bool
|
|
{
|
|
return hash_equals($this->generateVisitorHash($uid), $hash);
|
|
}
|
|
|
|
public function getKeyForJs(): string
|
|
{
|
|
return base64_encode($this->key);
|
|
}
|
|
}
|