Skip to content

Commit d06ac65

Browse files
committed
Add DummyHandler, NotAHandler, CrossHandlerTest, LegacyFormatRejectionTest, BaseHandlerTest, EncryptTest, EncryptionExceptionTest, OpenSSLTest, and SodiumTest
1 parent d5780c7 commit d06ac65

9 files changed

Lines changed: 1102 additions & 0 deletions

tests/Fixtures/DummyHandler.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Encryption\Tests\Fixtures;
6+
7+
use InitPHP\Encryption\BaseHandler;
8+
9+
/**
10+
* Minimal concrete handler used to drive {@see BaseHandler} from unit tests
11+
* without depending on the openssl or sodium extensions. The cryptographic
12+
* operations are stubbed; only the option and serialization machinery is real.
13+
*
14+
* Protected helpers are re-exposed as public methods so tests can assert on
15+
* them directly.
16+
*/
17+
final class DummyHandler extends BaseHandler
18+
{
19+
public function encrypt(mixed $data, array $options = []): string
20+
{
21+
$resolved = $this->resolveOptions($options);
22+
$flag = $this->serializerFlag($resolved);
23+
24+
return $this->serializePayload($data, $flag);
25+
}
26+
27+
public function decrypt(string $data, array $options = []): mixed
28+
{
29+
$resolved = $this->resolveOptions($options);
30+
$flag = $this->serializerFlag($resolved);
31+
32+
return $this->unserializePayload($data, $flag);
33+
}
34+
35+
/**
36+
* @param array<string, mixed> $options
37+
*/
38+
public function callRequireKey(array $options): string
39+
{
40+
return $this->requireKey($options);
41+
}
42+
43+
/**
44+
* @param array<string, mixed> $options
45+
*/
46+
public function callSerializerFlag(array $options): int
47+
{
48+
return $this->serializerFlag($options);
49+
}
50+
51+
public function callSerializePayload(mixed $data, int $flag): string
52+
{
53+
return $this->serializePayload($data, $flag);
54+
}
55+
56+
public function callUnserializePayload(string $data, int $flag): mixed
57+
{
58+
return $this->unserializePayload($data, $flag);
59+
}
60+
61+
/**
62+
* @param array<string, mixed> $options
63+
*
64+
* @return array<string, mixed>
65+
*/
66+
public function callResolveOptions(array $options): array
67+
{
68+
return $this->resolveOptions($options);
69+
}
70+
71+
public function jsonFlag(): int
72+
{
73+
return self::SERIALIZER_FLAG_JSON;
74+
}
75+
76+
public function phpFlag(): int
77+
{
78+
return self::SERIALIZER_FLAG_PHP;
79+
}
80+
}

tests/Fixtures/NotAHandler.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Encryption\Tests\Fixtures;
6+
7+
/**
8+
* A class that intentionally does not implement
9+
* {@see \InitPHP\Encryption\HandlerInterface}. Exists only to exercise the
10+
* "class is not a handler" error path of {@see \InitPHP\Encryption\Encrypt::use()}.
11+
*/
12+
final class NotAHandler
13+
{
14+
/**
15+
* @param array<string, mixed> $options
16+
*/
17+
public function __construct(public readonly array $options = [])
18+
{
19+
}
20+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Encryption\Tests\Integration;
6+
7+
use InitPHP\Encryption\Exceptions\EncryptionException;
8+
use InitPHP\Encryption\OpenSSL;
9+
use InitPHP\Encryption\Sodium;
10+
use PHPUnit\Framework\TestCase;
11+
12+
/**
13+
* Verifies that ciphertext from one handler cannot be misinterpreted by the
14+
* other. Each handler has its own on-wire layout following the shared 2-byte
15+
* header; the rest of the bytes are not interchangeable.
16+
*
17+
* @requires extension openssl
18+
* @requires extension sodium
19+
*/
20+
final class CrossHandlerTest extends TestCase
21+
{
22+
private const KEY = 'cross-handler-shared-key';
23+
24+
public function testOpenSSLCannotDecryptSodiumCiphertext(): void
25+
{
26+
$sodium = new Sodium(['key' => self::KEY]);
27+
$openssl = new OpenSSL(['key' => self::KEY]);
28+
$ciphertext = $sodium->encrypt(['payload' => 'cross']);
29+
30+
$this->expectException(EncryptionException::class);
31+
32+
$openssl->decrypt($ciphertext);
33+
}
34+
35+
public function testSodiumCannotDecryptOpenSSLCiphertext(): void
36+
{
37+
$openssl = new OpenSSL(['key' => self::KEY]);
38+
$sodium = new Sodium(['key' => self::KEY]);
39+
$ciphertext = $openssl->encrypt(['payload' => 'cross']);
40+
41+
$this->expectException(EncryptionException::class);
42+
43+
$sodium->decrypt($ciphertext);
44+
}
45+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InitPHP\Encryption\Tests\Integration;
6+
7+
use InitPHP\Encryption\BaseHandler;
8+
use InitPHP\Encryption\Exceptions\EncryptionException;
9+
use InitPHP\Encryption\OpenSSL;
10+
use InitPHP\Encryption\Sodium;
11+
use PHPUnit\Framework\TestCase;
12+
13+
/**
14+
* Documents the BC-break introduced in 2.0: ciphertexts produced by 1.x do
15+
* not carry a version byte, so feeding 1.x-shaped data into a 2.x handler
16+
* must produce a clear, recognisable error rather than a silent failure or
17+
* undefined behaviour.
18+
*
19+
* We cannot include real 1.x-produced ciphertexts without depending on the
20+
* 1.x release, so we synthesise input that LOOKS like 1.x output to a 2.x
21+
* parser: a hex-encoded blob whose first byte is something other than the
22+
* documented v2 magic.
23+
*
24+
* @requires extension openssl
25+
* @requires extension sodium
26+
*/
27+
final class LegacyFormatRejectionTest extends TestCase
28+
{
29+
private const KEY = 'legacy-format-test-key';
30+
31+
public function testOpenSSLRejectsV1ShapedCiphertext(): void
32+
{
33+
// Mimic 1.x output: hex-encoded blob without a 2-byte header, just
34+
// bytes whose first byte happens to be != FORMAT_VERSION.
35+
$fake = bin2hex(\chr(0x01) . random_bytes(64));
36+
37+
$handler = new OpenSSL(['key' => self::KEY]);
38+
39+
$this->expectException(EncryptionException::class);
40+
$this->expectExceptionMessage('Unsupported ciphertext format version');
41+
42+
$handler->decrypt($fake);
43+
}
44+
45+
public function testSodiumRejectsV1ShapedCiphertext(): void
46+
{
47+
$fake = bin2hex(\chr(0x01) . random_bytes(64));
48+
49+
$handler = new Sodium(['key' => self::KEY]);
50+
51+
$this->expectException(EncryptionException::class);
52+
$this->expectExceptionMessage('Unsupported ciphertext format version');
53+
54+
$handler->decrypt($fake);
55+
}
56+
57+
public function testErrorMessageGuidesUsersTowardsV2(): void
58+
{
59+
$fake = bin2hex(\chr(0x01) . random_bytes(64));
60+
$handler = new OpenSSL(['key' => self::KEY]);
61+
62+
try {
63+
$handler->decrypt($fake);
64+
self::fail('expected EncryptionException for v1 input');
65+
} catch (EncryptionException $e) {
66+
$message = $e->getMessage();
67+
68+
self::assertStringContainsString(\sprintf('0x%02x', BaseHandler::FORMAT_VERSION), $message);
69+
self::assertStringContainsString('1.x', $message);
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)