Files
xamxam/tests/phpunit/CryptoTest.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');
}
}