-
Notifications
You must be signed in to change notification settings - Fork 2
Testing Your Encryption
Encryption code is uniquely easy to get almost right and uniquely easy to never find out. A small set of property-style tests catches the mistakes that lead to silent data loss or to wrongly-trusted ciphertexts.
This page gives you a battery of tests modelled after the package's own suite. Drop them into your project's PHPUnit setup, adapt the handler construction to your bootstrap, and you have a regression net for whatever uses encryption.
These tests should exist for every handler instance you use in production.
The plaintext that goes in must come back out, unchanged:
public function testRoundTripPreservesValue(): void
{
$handler = $this->handlerUnderTest();
foreach ($this->payloadFixtures() as $label => $value) {
$ciphertext = $handler->encrypt($value);
$roundtrip = $handler->decrypt($ciphertext);
self::assertSame($value, $roundtrip, "round-trip failed for {$label}");
}
}
private function payloadFixtures(): iterable
{
yield 'string' => 'hello';
yield 'int' => 42;
yield 'array' => ['x' => 1, 'y' => [1, 2, 3]];
yield 'null' => null;
yield 'bool' => true;
}If you use 'serializer' => 'php_serialize' in your handler, add a
binary case:
yield 'binary' => "\x00\x01\xff raw bytes";JSON cannot round-trip raw binary — that test belongs only on the
php_serialize configuration.
Same plaintext, same handler — different ciphertext. If two calls return the same hex string, your IV / nonce randomisation is broken:
public function testCiphertextDiffersAcrossCalls(): void
{
$handler = $this->handlerUnderTest();
$a = $handler->encrypt('same plaintext');
$b = $handler->encrypt('same plaintext');
self::assertNotSame($a, $b);
}Flip a single byte. The handler must throw, not return garbage:
public function testDecryptRejectsTamperedCiphertext(): void
{
$handler = $this->handlerUnderTest();
$ct = $handler->encrypt('hello');
// Flip one hex character (= flip 4 bits of the underlying binary):
$tampered = $ct;
$tampered[-1] = $ct[-1] === '0' ? '1' : '0';
$this->expectException(\InitPHP\Encryption\Exceptions\EncryptionException::class);
$handler->decrypt($tampered);
}public function testWrongKeyCannotDecrypt(): void
{
$writer = new \InitPHP\Encryption\Sodium(['key' => 'key-a']);
$reader = new \InitPHP\Encryption\Sodium(['key' => 'key-b']);
$ct = $writer->encrypt('hello');
$this->expectException(\InitPHP\Encryption\Exceptions\EncryptionException::class);
$reader->decrypt($ct);
}public function testDecryptRejectsNonHex(): void
{
$this->expectException(\InitPHP\Encryption\Exceptions\EncryptionException::class);
$this->handlerUnderTest()->decrypt('not-hex-at-all-zzz');
}
public function testDecryptRejectsTruncated(): void
{
$this->expectException(\InitPHP\Encryption\Exceptions\EncryptionException::class);
$this->handlerUnderTest()->decrypt('ab'); // 1 binary byte, < header
}If a 1.x ciphertext ever ends up in your store, decryption must fail loudly, not silently:
public function testDecryptRejectsWrongVersionByte(): void
{
$handler = $this->handlerUnderTest();
$ct = $handler->encrypt('hello');
// Replace the version byte (first hex pair) with something else:
$forged = '01' . substr($ct, 2);
$this->expectException(\InitPHP\Encryption\Exceptions\EncryptionException::class);
$this->expectExceptionMessage('Unsupported ciphertext format version');
$handler->decrypt($forged);
}A subtle correctness invariant — if a per-call override leaked into the handler, two unrelated callers would step on each other:
public function testPerCallOptionsDoNotMutateHandlerState(): void
{
$handler = new \InitPHP\Encryption\OpenSSL([
'key' => 'k',
'cipher' => 'AES-256-CTR',
]);
$handler->encrypt('x', ['cipher' => 'AES-128-CTR']);
self::assertSame('AES-256-CTR', $handler->getOption('cipher'));
}public function testMissingKeyRaisesEncryptionException(): void
{
$handler = new \InitPHP\Encryption\Sodium();
$this->expectException(\InitPHP\Encryption\Exceptions\EncryptionException::class);
$this->expectExceptionMessage('"key"');
$handler->encrypt('hello');
}Useful when you are writing a custom handler or asserting compatibility guarantees in integration tests:
public function testCiphertextStartsWithFormatVersionByte(): void
{
$ct = $this->handlerUnderTest()->encrypt('hello');
$firstByte = hexdec(substr($ct, 0, 2));
self::assertSame(
\InitPHP\Encryption\BaseHandler::FORMAT_VERSION,
$firstByte
);
}public function testSerializerFlagMatchesConfiguredSerializer(): void
{
$json = new \InitPHP\Encryption\Sodium(['key' => 'k']); // default = json
$php = new \InitPHP\Encryption\Sodium([
'key' => 'k',
'serializer' => 'php_serialize',
]);
self::assertSame(0x00, hexdec(substr($json->encrypt('x'), 2, 2)));
self::assertSame(0x01, hexdec(substr($php->encrypt('x'), 2, 2)));
}If you need to test a BaseHandler subclass without depending on a real
cryptographic extension, use the same trick the package's own suite uses:
a DummyHandler that re-exposes the protected helpers.
namespace App\Tests\Fixtures;
use InitPHP\Encryption\BaseHandler;
final class DummyHandler extends BaseHandler
{
public function encrypt(mixed $data, array $options = []): string
{
$flag = $this->serializerFlag($this->resolveOptions($options));
return $this->serializePayload($data, $flag);
}
public function decrypt(string $data, array $options = []): mixed
{
$flag = $this->serializerFlag($this->resolveOptions($options));
return $this->unserializePayload($data, $flag);
}
public function callRequireKey(array $options): string
{
return $this->requireKey($options);
}
// ... add `callX()` wrappers for every protected helper you need.
}- The cryptographic primitives themselves. Don't write tests that assert specific ciphertext bytes for a fixed plaintext + key. Random IV/nonce makes that impossible, and even if you turned that off you would be testing OpenSSL / libsodium rather than your code.
- Speed. Performance regressions matter, but benchmark them in a separate script — PHPUnit timing is too noisy to fail builds on.
- Memory zeroing. PHP makes it impossible to assert "this byte is really gone from process memory" from user code. Trust the library.
- Run your encryption tests on every supported PHP version. PHP minor
releases occasionally change
unserializeorhash_*behaviour. - Run them with
failOnWarning="true"andfailOnDeprecation="true"in PHPUnit (this is what the package's own suite uses). A deprecation notice in a crypto path is something you want to see immediately, not in a year. - Consider running the suite under PHPStan or Psalm. The package ships with full type annotations; static analysis catches "I passed an array where a string was expected" before runtime does.
- Error Handling — the full catalogue of messages your tests can assert against.
- Custom Handlers — the protected helpers the fixture above re-exposes.
- Recipes — patterns to write integration tests for.
initphp/encryption · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Handlers
Reference
Practical Guides
Other