Skip to content

Commit 725e604

Browse files
authored
Merge pull request #17 from yassinedoghri/feat/obfuscator
feat(obfuscator): add class to generate payload for altcha-widget obfuscation
2 parents 293eb64 + d94f232 commit 725e604

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,31 @@ Finds a solution to the given challenge.
131131

132132
**Returns:** `null|Solution`
133133

134+
## Generate obfuscation payload
135+
136+
Generate an obfuscated payload for client-side clarification:
137+
138+
```php
139+
<?php
140+
141+
// With optional maxNumber (defaults to 10_000)
142+
$obfuscator = new \AltchaOrg\Altcha\Obfuscator();
143+
144+
// Text to reveal after client-side PoW
145+
$plaintext = 'mailto:[email protected]';
146+
147+
// Optional shared key
148+
$key = 'shared-secret';
149+
150+
// Optionally fix the counter; omit to use a random counter in [0, maxNumber]
151+
$fixedCounter = null;
152+
153+
// Generate base64 obfuscated payload
154+
$payload = $obfuscator->obfuscateData($plaintext, $key, $fixedCounter);
155+
156+
echo $payload;
157+
// P7bJsUgzxP416d1voeF/QnQOD5g7GItB/zdfkoBrKgZK4N4IYkDJqg==
158+
```
134159

135160
## Tests
136161

src/Obfuscator.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
}

tests/AltchaTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use AltchaOrg\Altcha\ChallengeOptions;
77
use AltchaOrg\Altcha\Hasher\Algorithm;
88
use AltchaOrg\Altcha\Hasher\Hasher;
9+
use AltchaOrg\Altcha\Obfuscator;
910
use AltchaOrg\Altcha\Solution;
1011
use PHPUnit\Framework\TestCase;
1112

@@ -217,4 +218,27 @@ public function testInvalidPayloads(): void
217218
]);
218219
self::assertFalse($verification->verified);
219220
}
221+
222+
public function testDataObfuscation(): void
223+
{
224+
$obfuscator = new Obfuscator();
225+
$counter = 1234;
226+
227+
$email = 'mailto:[email protected]';
228+
$address = 'Big Company Ltd.' . \PHP_EOL . '123 Baker Street' . \PHP_EOL . 'London' . \PHP_EOL . 'NW1 6XE' . \PHP_EOL . 'United Kingdom';
229+
230+
$obfuscatedEmail = $obfuscator->obfuscateData($email, '', $counter);
231+
$obfuscatedAddress = $obfuscator->obfuscateData($address, '', $counter);
232+
233+
self::assertEquals('0WkWnouD/fos4DdkwHP2yQ1/UejeYYUjP2vtH+F5+s8PYTJcvup6mg==', $obfuscatedEmail);
234+
self::assertEquals('/mEY0ryDquIo4iIrzGLqhmo+D77QQIslSwgQNW1ojHqPmhU6DxlmZfKkXAfiCR2BabAg7E/K7kPttHA/c5sDe08H1tR822PCVNxRwGbVOg==', $obfuscatedAddress);
235+
236+
$key = 'shared-secret';
237+
238+
$obfuscatedEmailWithKey = $obfuscator->obfuscateData($email, $key, $counter);
239+
$obfuscatedAddressWithKey = $obfuscator->obfuscateData($address, $key, $counter);
240+
241+
self::assertEquals('VYjBMLSjm0dasqAtnQvSlRiqG0V0paCoLFtNPZTB5jwmjPmV2Ja41w==', $obfuscatedEmailWithKey);
242+
self::assertEquals('eoDPfIOjzF9esLVikRrO2n/rRRN6hK6ug4cwoWDJHYS9++OfBQjOUMP9oBHiGbNR0+3pj2whifZf/Cixj6oG+ybIQjx/t2apDsSLwr4qKg==', $obfuscatedAddressWithKey);
243+
}
220244
}

0 commit comments

Comments
 (0)