mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
153 lines
5.6 KiB
PHP
153 lines
5.6 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* CryptoTest — Pure logic tests for the Crypto class (AES-256-GCM).
|
|
*
|
|
* Requires a valid APP_KEY in app/.env (set up by bootstrap.php).
|
|
*/
|
|
class CryptoTest extends TestCase
|
|
{
|
|
// ── Encrypt → Decrypt round-trip ──────────────────────────────────────────
|
|
|
|
public function testEncryptDecryptRoundTrip(): void
|
|
{
|
|
$plain = 'Hello, World!';
|
|
$enc = Crypto::encrypt($plain);
|
|
$dec = Crypto::decrypt($enc);
|
|
|
|
$this->assertSame($plain, $dec);
|
|
$this->assertNotSame($plain, $enc, 'Ciphertext must differ from plaintext');
|
|
}
|
|
|
|
public function testEncryptDecryptWithUnicode(): void
|
|
{
|
|
$plain = 'émoji 🎉 — français & 日本語';
|
|
$enc = Crypto::encrypt($plain);
|
|
$dec = Crypto::decrypt($enc);
|
|
|
|
$this->assertSame($plain, $dec);
|
|
}
|
|
|
|
public function testEncryptDecryptMultiline(): void
|
|
{
|
|
$plain = "Line 1\nLine 2\r\nLine 3\n\n";
|
|
$enc = Crypto::encrypt($plain);
|
|
$dec = Crypto::decrypt($enc);
|
|
|
|
$this->assertSame($plain, $dec);
|
|
}
|
|
|
|
// ── Different plaintexts → different ciphertexts ──────────────────────────
|
|
|
|
public function testDifferentPlaintextsProduceDifferentCiphertexts(): void
|
|
{
|
|
$a = Crypto::encrypt('alpha');
|
|
$b = Crypto::encrypt('beta');
|
|
|
|
$this->assertNotSame($a, $b);
|
|
}
|
|
|
|
public function testSamePlaintextProducesDifferentCiphertexts(): void
|
|
{
|
|
// IV is random every time — encrypting the same value twice should
|
|
// never produce identical ciphertext (except in astronomically rare
|
|
// collisions of a 12-byte IV).
|
|
$a = Crypto::encrypt('same');
|
|
$b = Crypto::encrypt('same');
|
|
|
|
$this->assertNotSame($a, $b, 'IV randomness → ciphertexts must differ');
|
|
}
|
|
|
|
// ── isEncrypted() ─────────────────────────────────────────────────────────
|
|
|
|
public function testIsEncryptedRecognizesEncryptedValue(): void
|
|
{
|
|
$enc = Crypto::encrypt('secret');
|
|
$this->assertTrue(Crypto::isEncrypted($enc));
|
|
}
|
|
|
|
public function testIsEncryptedRejectsPlaintext(): void
|
|
{
|
|
$this->assertFalse(Crypto::isEncrypted('plaintext password'));
|
|
}
|
|
|
|
public function testIsEncryptedReturnsFalseForEmptyString(): void
|
|
{
|
|
$this->assertFalse(Crypto::isEncrypted(''));
|
|
}
|
|
|
|
public function testIsEncryptedRejectsInvalidBase64(): void
|
|
{
|
|
$this->assertFalse(Crypto::isEncrypted('not valid base64!!!'));
|
|
}
|
|
|
|
// ── Empty string ──────────────────────────────────────────────────────────
|
|
|
|
public function testEncryptEmptyStringProducesCiphertext(): void
|
|
{
|
|
$enc = Crypto::encrypt('');
|
|
$this->assertNotEmpty($enc, 'Encrypting empty string should still produce ciphertext');
|
|
// Note: isEncrypted() returns false for the empty-string ciphertext
|
|
// because the raw blob (IV+TAG+empty ciphertext) is exactly 28 bytes,
|
|
// which is < IV_LEN + TAG_LEN + 1 (29). This is a known edge case.
|
|
$raw = base64_decode($enc, true);
|
|
$this->assertIsString($raw);
|
|
$this->assertEquals(28, strlen($raw), 'IV(12) + TAG(16) + empty ciphertext(0)');
|
|
}
|
|
|
|
public function testDecryptEmptyStringReturnsEmpty(): void
|
|
{
|
|
$this->assertSame('', Crypto::decrypt(''));
|
|
}
|
|
|
|
// ── Invalid base64 input ──────────────────────────────────────────────────
|
|
|
|
public function testDecryptInvalidBase64ReturnsInputGracefully(): void
|
|
{
|
|
// Legacy fallback: non-base64 strings are returned as-is
|
|
$input = 'plaintext-legacy-password';
|
|
$result = Crypto::decrypt($input);
|
|
$this->assertSame($input, $result);
|
|
}
|
|
|
|
public function testDecryptTooShortBlobReturnsInputGracefully(): void
|
|
{
|
|
// Base64 blob that decodes to fewer bytes than IV + TAG + 1
|
|
$tooShort = base64_encode('abc');
|
|
$result = Crypto::decrypt($tooShort);
|
|
$this->assertSame($tooShort, $result);
|
|
}
|
|
|
|
// ── Wrong key / tampering ─────────────────────────────────────────────────
|
|
|
|
public function testDecryptWithTamperedCiphertextReturnsEmpty(): void
|
|
{
|
|
$enc = Crypto::encrypt('tamper me');
|
|
$raw = base64_decode($enc, true);
|
|
|
|
// Flip a byte inside the ciphertext portion (after IV + tag)
|
|
$tamperOffset = 12 + 16; // after 12-byte IV + 16-byte tag
|
|
$raw[$tamperOffset] = chr(ord($raw[$tamperOffset]) ^ 0xFF);
|
|
|
|
$tampered = base64_encode($raw);
|
|
$result = Crypto::decrypt($tampered);
|
|
$this->assertSame('', $result, 'Tampered ciphertext must return empty string');
|
|
}
|
|
|
|
public function testDecryptValidBlobTamperedTagReturnsEmpty(): void
|
|
{
|
|
$enc = Crypto::encrypt('test');
|
|
$raw = base64_decode($enc, true);
|
|
|
|
// Tamper with the authentication tag (bytes 13-28, after the 12-byte IV)
|
|
$raw[13] = chr(ord($raw[13]) ^ 0xFF);
|
|
|
|
$tampered = base64_encode($raw);
|
|
$result = Crypto::decrypt($tampered);
|
|
|
|
$this->assertSame('', $result, 'Tag mismatch must return empty');
|
|
}
|
|
}
|