ercsctt/laravel-file-encryption

Secure file encryption and decryption for Laravel applications

Installs: 376

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/ercsctt/laravel-file-encryption

v1.0.0 2026-01-19 17:26 UTC

This package is auto-updated.

Last update: 2026-01-19 17:43:50 UTC


README

A robust file encryption package for Laravel using AES-256-GCM authenticated encryption. Designed for secure, efficient encryption and decryption of files of any size with streaming support for memory-efficient processing of large files.

Features

  • AES-256-GCM encryption - Industry-standard authenticated encryption
  • Streaming support - Process files of any size with minimal memory usage
  • Key rotation - Seamlessly rotate encryption keys without downtime
  • Artisan commands - Encrypt and decrypt files from the command line
  • Facade and helper - Convenient ways to use the package

Requirements

  • PHP 8.2 or higher
  • Laravel 11.0 or higher
  • OpenSSL PHP extension

Installation

Install the package via Composer:

composer require ercsctt/laravel-file-encryption

The package will automatically register its service provider.

Configuration

Publish the configuration file:

php artisan vendor:publish --tag=file-encryption-config

This will create a config/file-encryption.php file.

Environment Variables

Add the following to your .env file:

FILE_ENCRYPTION_KEY=base64:your-base64-encoded-32-byte-key

Generate a key using:

php artisan tinker --execute="echo 'base64:'.base64_encode(random_bytes(32));"

Optional configuration:

# Previous keys for key rotation (comma-separated, base64-encoded)
FILE_ENCRYPTION_PREVIOUS_KEYS=base64:old-key-1,base64:old-key-2

# Chunk size for streaming (default: 65536 bytes)
FILE_ENCRYPTION_CHUNK_SIZE=65536

Usage

Using the Facade

use Ercsctt\FileEncryption\Facades\FileEncrypter;

// Encrypt a file
FileEncrypter::encryptFile('/path/to/source.txt', '/path/to/encrypted.enc');

// Decrypt a file
FileEncrypter::decryptFile('/path/to/encrypted.enc', '/path/to/decrypted.txt');

// Auto-generate destination path (appends .enc for encryption)
FileEncrypter::encryptFile('/path/to/source.txt'); // Creates source.txt.enc

// Auto-generate destination path (removes .enc for decryption)
FileEncrypter::decryptFile('/path/to/source.txt.enc'); // Creates source.txt

// Get decrypted contents as string
$contents = FileEncrypter::decryptedContents('/path/to/encrypted.enc');

// Check if a file is encrypted
if (FileEncrypter::isEncrypted('/path/to/file.enc')) {
    // File has encryption magic bytes
}

Using the Helper Function

// Decrypt and get file contents
$contents = decrypt_file('/path/to/encrypted.enc');

Direct Instantiation

use Ercsctt\FileEncryption\FileEncrypter;

// Key must be exactly 32 bytes for AES-256
$key = random_bytes(32);
$encrypter = new FileEncrypter($key);

// With custom chunk size (minimum 1024 bytes)
$encrypter = new FileEncrypter($key, 131072); // 128KB chunks

// Encrypt and decrypt
$encrypter->encryptFile('/path/to/source.txt', '/path/to/encrypted.enc');
$encrypter->decryptFile('/path/to/encrypted.enc', '/path/to/decrypted.txt');

Artisan Commands

Encrypt Command

php artisan file:encrypt {path} [options]

Options:

Option Description
--key= Encryption key (base64-encoded). Uses configured key if not provided
-R, --recursive Recursively encrypt all files in a directory
--force Skip confirmation prompts
--chunk-size=65536 Chunk size in bytes for streaming
--prune Delete original files after successful encryption

Examples:

# Encrypt a single file (creates document.pdf.enc)
php artisan file:encrypt storage/app/document.pdf

# Encrypt with a specific key
php artisan file:encrypt storage/app/secret.txt --key="base64:abc123..."

# Encrypt an entire directory recursively
php artisan file:encrypt storage/app/private --recursive

# Encrypt directory and delete originals after encryption
php artisan file:encrypt storage/app/sensitive --recursive --prune

# Encrypt without confirmation prompts (useful for scripts/CI)
php artisan file:encrypt storage/app/data --recursive --force

# Encrypt with larger chunks for better performance on big files
php artisan file:encrypt storage/app/large-video.mp4 --chunk-size=1048576

# Combine options: encrypt directory, delete originals, no prompts
php artisan file:encrypt storage/app/backup --recursive --prune --force

Decrypt Command

php artisan file:decrypt {path?} [options]

Options:

Option Description
--key= Decryption key (base64-encoded)
-R, --recursive Recursively decrypt all .enc files in a directory
--force Skip confirmation prompts
--keep Keep encrypted files after successful decryption
--output= Custom output path for decrypted file
--scan Scan entire project for encrypted (.enc) files

Examples:

# Decrypt a single file (creates document.pdf from document.pdf.enc)
php artisan file:decrypt storage/app/document.pdf.enc

# Decrypt with a specific key
php artisan file:decrypt storage/app/secret.txt.enc --key="base64:abc123..."

# Decrypt to a custom output path
php artisan file:decrypt storage/app/config.json.enc --output=config/decrypted.json

# Decrypt an entire directory recursively
php artisan file:decrypt storage/app/private --recursive

# Decrypt but keep the encrypted files (useful for verification)
php artisan file:decrypt storage/app/backup --recursive --keep

# Decrypt without confirmation prompts
php artisan file:decrypt storage/app/data.enc --force

# Scan entire project for encrypted files and decrypt them
php artisan file:decrypt --scan

# Scan and decrypt all found files, keeping encrypted versions
php artisan file:decrypt --scan --keep

# Scan and decrypt without prompts (CI/scripts)
php artisan file:decrypt --scan --force

# Combine options: decrypt directory recursively, no prompts, keep originals
php artisan file:decrypt storage/app/encrypted --recursive --force --keep

Common Workflows

Encrypt sensitive uploads for storage:

# Encrypt all files in uploads directory, remove originals
php artisan file:encrypt storage/app/uploads --recursive --prune --force

Decrypt files for processing, then re-encrypt:

# Decrypt keeping encrypted versions
php artisan file:decrypt storage/app/data --recursive --keep --force

# ... process the files ...

# Re-encrypt (original .enc files still exist as backup)
php artisan file:encrypt storage/app/data --recursive --prune --force

Find and decrypt all encrypted files in a project:

# Scan finds all .enc files in the project and decrypts them
php artisan file:decrypt --scan --force

Migrate to a new encryption key:

# 1. Set new key in .env, add old key to FILE_ENCRYPTION_PREVIOUS_KEYS
# 2. Decrypt all files (will use old key automatically)
php artisan file:decrypt storage/app/encrypted --recursive --force

# 3. Re-encrypt with new key
php artisan file:encrypt storage/app/encrypted --recursive --prune --force

Streaming Large Files

For large files, use decryptedStream() for memory-efficient processing:

use Ercsctt\FileEncryption\Facades\FileEncrypter;

// Stream decrypted content through a callback
FileEncrypter::decryptedStream('/path/to/large-file.enc', function ($chunk) {
    echo $chunk; // Process each chunk
});

// Stream to a file handle
$output = fopen('/path/to/output.txt', 'wb');
FileEncrypter::decryptedStream('/path/to/encrypted.enc', function ($chunk) use ($output) {
    fwrite($output, $chunk);
});
fclose($output);

// Stream for hashing
$context = hash_init('sha256');
FileEncrypter::decryptedStream('/path/to/file.enc', function ($chunk) use ($context) {
    hash_update($context, $chunk);
});
$hash = hash_final($context);

Progress Callbacks

Monitor progress when processing large files:

use Ercsctt\FileEncryption\Facades\FileEncrypter;

// Progress callback receives current chunk and total chunks
FileEncrypter::encryptFile(
    '/path/to/large-file.zip',
    '/path/to/encrypted.enc',
    function ($currentChunk, $totalChunks) {
        $percent = round(($currentChunk / $totalChunks) * 100);
        echo "Encrypting: {$percent}%\n";
    }
);

FileEncrypter::decryptFile(
    '/path/to/encrypted.enc',
    '/path/to/decrypted.zip',
    function ($currentChunk, $totalChunks) {
        echo "Decrypting chunk {$currentChunk} of {$totalChunks}\n";
    }
);

Key Rotation

When you need to rotate your encryption key:

  1. Generate a new key:

    php artisan tinker --execute="echo 'base64:'.base64_encode(random_bytes(32));"
  2. Update your .env file:

    FILE_ENCRYPTION_KEY=base64:your-new-key
    FILE_ENCRYPTION_PREVIOUS_KEYS=base64:your-old-key
  3. The package will automatically try previous keys when decrypting files encrypted with old keys.

  4. Programmatically configure previous keys:

    use Ercsctt\FileEncryption\FileEncrypter;
    
    $currentKey = random_bytes(32);
    $oldKey = '...'; // Your previous 32-byte key
    
    $encrypter = (new FileEncrypter($currentKey))->previousKeys([$oldKey]);
    
    // Decryption will try current key first, then fall back to old keys
    $encrypter->decryptFile('/path/to/old-encrypted.enc', '/path/to/decrypted.txt');
    
    // Get all configured keys
    $allKeys = $encrypter->getAllKeys(); // Returns [currentKey, oldKey]

Security Features

  • AES-256-GCM - Provides both confidentiality and authenticity
  • Unique nonce per chunk - Each chunk uses a unique 12-byte nonce derived from base nonce XORed with chunk index
  • Authentication tag - 16-byte GCM authentication tag per chunk prevents tampering
  • Chunk index in AAD - Additional authenticated data includes chunk index to prevent reordering attacks
  • Header HMAC - 12-byte truncated HMAC-SHA256 protects header integrity
  • Constant-time comparisons - Uses hash_equals() to prevent timing attacks
  • Secure key handling - Keys marked with #[\SensitiveParameter] attribute

File Format

Encrypted files use the following binary format:

HEADER (32 bytes):
  [4 bytes: Magic "LENC"]
  [1 byte: Version]
  [1 byte: Cipher ID (1 = AES-256-GCM)]
  [2 bytes: Reserved]
  [4 bytes: Chunk size (big-endian)]
  [8 bytes: Original file size (big-endian)]
  [12 bytes: Header HMAC]

CHUNKS (repeated):
  [12 bytes: Nonce]
  [N bytes: Ciphertext]
  [16 bytes: GCM authentication tag]

Error Handling

The package throws specific exceptions for different error conditions:

use Ercsctt\FileEncryption\Facades\FileEncrypter;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Contracts\Encryption\DecryptException;

try {
    FileEncrypter::encryptFile('/path/to/source.txt', '/path/to/encrypted.enc');
} catch (EncryptException $e) {
    // Handle encryption failure (file not found, not readable, etc.)
}

try {
    FileEncrypter::decryptFile('/path/to/encrypted.enc', '/path/to/decrypted.txt');
} catch (DecryptException $e) {
    // Handle decryption failure (wrong key, corrupted file, invalid format, etc.)
}

Key Validation

use Ercsctt\FileEncryption\FileEncrypter;

// Keys must be exactly 32 bytes
try {
    new FileEncrypter(str_repeat('a', 16)); // Too short!
} catch (RuntimeException $e) {
    echo $e->getMessage(); // "File encryption requires a 32-byte key for AES-256-GCM."
}

// Chunk size must be at least 1024 bytes
try {
    new FileEncrypter(random_bytes(32), 512); // Too small!
} catch (RuntimeException $e) {
    echo $e->getMessage(); // "Chunk size must be at least 1024 bytes."
}

Testing

Run the test suite:

composer test

Or with PHPUnit directly:

./vendor/bin/phpunit

API Reference

FileEncrypter Methods

Method Signature Description
encryptFile (string $source, ?string $dest = null, ?callable $progress = null): void Encrypt a file. Destination defaults to $source.enc
decryptFile (string $source, ?string $dest = null, ?callable $progress = null): void Decrypt a file. Destination defaults to source without .enc
decryptedContents (string $path): string Return entire decrypted file contents as string
decryptedStream (string $path, callable $callback): void Stream decrypted chunks through callback
isEncrypted (string $path): bool Check if file has encryption magic bytes
previousKeys (array $keys): static Set previous keys for key rotation (fluent)
getKey (): string Get primary encryption key
getChunkSize (): int Get configured chunk size
getAllKeys (): array Get all keys (primary + previous)

License

This package is open-sourced software licensed under the MIT license.