Files
SealShare/tests/Unit/FileEncryptionServiceTest.php
T
Andreas Reinhold / reiniandClaude Opus 4.8 9f929c7a78 Apply Pint 1.29 formatting
The Laravel 13 upgrade pulled Pint 1.27.1 to 1.29.3, which changed the
laravel preset's default rules. composer lint and the CI lint job fail
without this, so it is required rather than cosmetic.

Formatting only, applied by `vendor/bin/pint`. The rules that fired were
fully_qualified_strict_types, ordered_imports, single_blank_line_at_eof,
unary_operator_spaces, not_operator_with_successor_space, braces_position,
single_line_empty_body, single_line_after_imports and no_extra_blank_lines.

Kept separate from the upgrade commit to keep that diff reviewable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:16:59 +02:00

237 lines
7.8 KiB
PHP

<?php
use App\Services\FileEncryptionService;
use Symfony\Component\HttpFoundation\StreamedResponse;
beforeEach(function () {
$this->service = new FileEncryptionService;
$this->tempDir = sys_get_temp_dir().'/sealshare-test-'.uniqid();
mkdir($this->tempDir, 0755, true);
});
afterEach(function () {
if (is_dir($this->tempDir)) {
array_map('unlink', glob($this->tempDir.'/*'));
rmdir($this->tempDir);
}
});
test('encrypt and decrypt round-trip works', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
$content = 'Hello, World! This is a secret message.';
file_put_contents($sourcePath, $content);
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
expect(file_exists($encryptedPath))->toBeTrue();
expect(file_get_contents($encryptedPath))->not->toBe($content);
$decrypted = $this->service->decryptFile($encryptedPath, $key);
expect($decrypted)->toBe($content);
});
test('decrypt with wrong key fails', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
file_put_contents($sourcePath, 'Secret data');
$correctKey = $this->service->generateRandomKey();
$wrongKey = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $correctKey);
$this->service->decryptFile($encryptedPath, $wrongKey);
})->throws(RuntimeException::class, 'Decryption failed');
test('derive key produces consistent results', function () {
$password = 'my-secure-password';
$salt = $this->service->generateSalt();
$key1 = $this->service->deriveKey($password, $salt);
$key2 = $this->service->deriveKey($password, $salt);
expect($key1)->toBe($key2);
});
test('derive key with different passwords produces different keys', function () {
$salt = $this->service->generateSalt();
$key1 = $this->service->deriveKey('password1', $salt);
$key2 = $this->service->deriveKey('password2', $salt);
expect($key1)->not->toBe($key2);
});
test('derive key with different salts produces different keys', function () {
$password = 'same-password';
$key1 = $this->service->deriveKey($password, $this->service->generateSalt());
$key2 = $this->service->deriveKey($password, $this->service->generateSalt());
expect($key1)->not->toBe($key2);
});
test('generate random key returns 64 char hex string', function () {
$key = $this->service->generateRandomKey();
expect(strlen($key))->toBe(64);
expect(ctype_xdigit($key))->toBeTrue();
});
test('generate salt returns 64 char hex string', function () {
$salt = $this->service->generateSalt();
expect(strlen($salt))->toBe(64);
expect(ctype_xdigit($salt))->toBeTrue();
});
test('password-derived key encrypt/decrypt round-trip works', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
$content = 'Password protected content';
file_put_contents($sourcePath, $content);
$password = 'user-password';
$salt = $this->service->generateSalt();
$key = bin2hex($this->service->deriveKey($password, $salt));
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$decrypted = $this->service->decryptFile($encryptedPath, $key);
expect($decrypted)->toBe($content);
});
test('decrypt file stream returns streamed response', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
$content = 'Streamed content';
file_put_contents($sourcePath, $content);
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$response = $this->service->decryptFileStream($encryptedPath, $key, 'test.txt', 'text/plain');
expect($response)->toBeInstanceOf(StreamedResponse::class);
expect($response->headers->get('Content-Type'))->toBe('text/plain');
expect($response->headers->get('Content-Disposition'))->toContain('test.txt');
});
test('chunked file has SEALCHK1 magic header', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
file_put_contents($sourcePath, 'test content');
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$header = file_get_contents($encryptedPath, false, null, 0, 8);
expect($header)->toBe('SEALCHK1');
});
test('multi-chunk round-trip works', function () {
$sourcePath = $this->tempDir.'/large.bin';
$encryptedPath = $this->tempDir.'/large.enc';
// Create a file larger than one 4 MB chunk (5 MB)
$chunkSize = 4 * 1024 * 1024;
$content = random_bytes($chunkSize + (1024 * 1024));
file_put_contents($sourcePath, $content);
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$decrypted = $this->service->decryptFile($encryptedPath, $key);
expect($decrypted)->toBe($content);
});
test('exact chunk boundary round-trip works', function () {
$sourcePath = $this->tempDir.'/exact.bin';
$encryptedPath = $this->tempDir.'/exact.enc';
// Create a file exactly equal to one chunk (4 MB)
$content = random_bytes(4 * 1024 * 1024);
file_put_contents($sourcePath, $content);
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$decrypted = $this->service->decryptFile($encryptedPath, $key);
expect($decrypted)->toBe($content);
});
test('empty file round-trip works', function () {
$sourcePath = $this->tempDir.'/empty.bin';
$encryptedPath = $this->tempDir.'/empty.enc';
file_put_contents($sourcePath, '');
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$decrypted = $this->service->decryptFile($encryptedPath, $key);
expect($decrypted)->toBe('');
});
test('legacy format backward compatibility', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/legacy.enc';
$content = 'Legacy encrypted content';
file_put_contents($sourcePath, $content);
$key = $this->service->generateRandomKey();
$binaryKey = hex2bin($key);
// Manually create a legacy format file: [nonce][tag][ciphertext]
$nonce = random_bytes(12);
$tag = '';
$ciphertext = openssl_encrypt($content, 'aes-256-gcm', $binaryKey, OPENSSL_RAW_DATA, $nonce, $tag, '', 16);
file_put_contents($encryptedPath, $nonce.$tag.$ciphertext);
$decrypted = $this->service->decryptFile($encryptedPath, $key);
expect($decrypted)->toBe($content);
});
test('wrong key on chunked file throws exception', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
file_put_contents($sourcePath, 'Chunked secret data');
$correctKey = $this->service->generateRandomKey();
$wrongKey = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $correctKey);
$this->service->decryptFile($encryptedPath, $wrongKey);
})->throws(RuntimeException::class, 'Decryption failed');
test('decrypt file stream with file size sets content-length header', function () {
$sourcePath = $this->tempDir.'/source.txt';
$encryptedPath = $this->tempDir.'/encrypted.enc';
$content = 'Content with known size';
file_put_contents($sourcePath, $content);
$key = $this->service->generateRandomKey();
$this->service->encryptFile($sourcePath, $encryptedPath, $key);
$response = $this->service->decryptFileStream($encryptedPath, $key, 'test.txt', 'text/plain', strlen($content));
expect($response->headers->get('Content-Length'))->toBe((string) strlen($content));
});