|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace AltchaOrg\Altcha; |
| 6 | + |
| 7 | +use AltchaOrg\Altcha\Hasher\Algorithm; |
| 8 | +use AltchaOrg\Altcha\Hasher\Hasher; |
| 9 | +use AltchaOrg\Altcha\Hasher\HasherInterface; |
| 10 | + |
| 11 | +class Obfuscator |
| 12 | +{ |
| 13 | + public const DEFAULT_MAX_NUMBER = 10_000; |
| 14 | + |
| 15 | + /** |
| 16 | + * @param int $maxNumber Maximum number for the random number generator (default: 10,000) |
| 17 | + */ |
| 18 | + public function __construct( |
| 19 | + private readonly int $maxNumber = self::DEFAULT_MAX_NUMBER, |
| 20 | + private readonly HasherInterface $hasher = new Hasher(), |
| 21 | + ) { |
| 22 | + } |
| 23 | + |
| 24 | + /** |
| 25 | + * Encrypts a payload for PoW-based reveal using AES-GCM. |
| 26 | + * |
| 27 | + * @param string $raw Plaintext payload to encrypt. |
| 28 | + * @param string $key Symmetric key used to encrypt/decrypt the payload. Defaults to empty string, ie. no key. |
| 29 | + * @param null|int $counter Optional fixed PoW counter used to derive the IV. If null, a random integer in [0, $this->maxNumber] inclusive is chosen. |
| 30 | + * |
| 31 | + * @return string Base64-encoded bytes of ciphertext followed by the 16‑byte GCM authentication tag ($ciphertext . $tag). |
| 32 | + */ |
| 33 | + public function obfuscateData(string $raw, string $key = '', ?int $counter = null): string |
| 34 | + { |
| 35 | + $cipher = 'AES-256-GCM'; |
| 36 | + $keyHash = $this->hasher->hash(Algorithm::SHA256, $key); |
| 37 | + |
| 38 | + $ivLength = openssl_cipher_iv_length($cipher); |
| 39 | + |
| 40 | + if (false === $ivLength) { |
| 41 | + throw new \RuntimeException('Getting cipher iv length failed.'); |
| 42 | + } |
| 43 | + |
| 44 | + $iv = ''; // AES‑GCM initialization vector (IV), typically 12 bytes for AES-256-GCM |
| 45 | + $num = $counter ?? $this->randomInt(); |
| 46 | + |
| 47 | + // Fill IV from the counter, one byte at a time (little‑endian) |
| 48 | + for ($i = 0; $i < $ivLength; $i++) { |
| 49 | + $iv .= \chr($num % 256); |
| 50 | + $num = intdiv($num, 256); |
| 51 | + } |
| 52 | + |
| 53 | + $encryptedData = openssl_encrypt($raw, $cipher, $keyHash, \OPENSSL_RAW_DATA, $iv, $tag); |
| 54 | + |
| 55 | + if (!$encryptedData) { |
| 56 | + throw new \RuntimeException('Data encryption failed.'); |
| 57 | + } |
| 58 | + |
| 59 | + return base64_encode($encryptedData . $tag); |
| 60 | + } |
| 61 | + |
| 62 | + private function randomInt(): int |
| 63 | + { |
| 64 | + return random_int(0, $this->maxNumber); |
| 65 | + } |
| 66 | +} |
0 commit comments