caeligo/field-encryption-bundle
This document covers the encryption of binary files (documents, images, etc.) stored in BLOB fields using AES-256-GCM.
Binary file encryption is designed for larger binary data like PDFs, images, documents, etc. The bundle uses:
<?php
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Caeligo\FieldEncryptionBundle\Attribute\EncryptedEntity;
use Caeligo\FieldEncryptionBundle\Attribute\EncryptedFile;
use Caeligo\FieldEncryptionBundle\Model\EncryptedFileData;
#[ORM\Entity]
#[EncryptedEntity]
class Document
{
#[ORM\Id]
#[ORM\Column(type: 'ulid', unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UlidGenerator::class)]
private ?Ulid $id = null;
#[ORM\Column(type: Types::BLOB, nullable: true)]
#[EncryptedFile(
mimeTypeProperty: 'mimeType',
originalNameProperty: 'originalName',
originalSizeProperty: 'originalSize',
)]
private $content;
#[ORM\Column(type: Types::STRING, length: 100, nullable: true)]
private ?string $mimeType = null;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
private ?string $originalName = null;
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $originalSize = null;
private ?EncryptedFileData $plainContent = null;
public function getPlainContent(): ?EncryptedFileData
{
return $this->plainContent;
}
public function setPlainContent(?EncryptedFileData $content): self
{
$this->plainContent = $content;
return $this;
}
public function getMimeType(): ?string
{
return $this->mimeType;
}
public function getOriginalName(): ?string
{
return $this->originalName;
}
public function getOriginalSize(): ?int
{
return $this->originalSize;
}
}
#[EncryptedFile]#[EncryptedFile(
plainType: 'dto', // 'dto' (EncryptedFileData) or 'string' (raw binary)
plainProperty: 'plainContent', // Default: 'plain' + PropertyName
mimeTypeProperty: 'mimeType', // Property to store MIME type (optional)
originalNameProperty: 'name', // Property to store filename (optional)
originalSizeProperty: 'size', // Property to store file size (optional)
compress: true, // Enable gzip compression (optional)
maxSize: 5242880, // Max file size in bytes (default: 5MB)
chunkSize: 163840, // Encryption chunk size (default: 160KB)
)]
| Parameter | Type | Default | Description |
|---|---|---|---|
plainType |
string | 'dto' |
Type of plain property: 'dto' or 'string' |
plainProperty |
string|null | Auto | Transient property name for decrypted data |
mimeTypeProperty |
string|null | null | Property to store MIME type |
originalNameProperty |
string|null | null | Property to store original filename |
originalSizeProperty |
string|null | null | Property to store original file size |
compress |
bool|null | null | Compress before encryption (null = bundle default) |
maxSize |
int|null | 5MB | Maximum allowed file size |
chunkSize |
int|null | 160KB | Chunk size for processing |
The EncryptedFileData DTO provides convenient methods for working with file data.
use Caeligo\FieldEncryptionBundle\Model\EncryptedFileData;
// From Symfony UploadedFile (most common)
$fileData = EncryptedFileData::fromUploadedFile($uploadedFile);
// From file path
$fileData = EncryptedFileData::fromPath('/path/to/document.pdf');
// From base64 string
$fileData = EncryptedFileData::fromBase64($base64Content, 'application/pdf', 'document.pdf');
// From data URI
$fileData = EncryptedFileData::fromDataUri('data:image/png;base64,...', 'image.png');
// Manual construction
$fileData = new EncryptedFileData(
content: $binaryContent,
mimeType: 'application/pdf',
originalName: 'document.pdf',
size: strlen($binaryContent)
);
// Set on entity
$document->setPlainContent($fileData);
$entityManager->persist($document);
$entityManager->flush();
// Retrieve and use
$fileData = $document->getPlainContent();
// Access content
$content = $fileData->getContent(); // Raw binary
$base64 = $fileData->toBase64(); // Base64 string
$dataUri = $fileData->toDataUri(); // Data URI for embedding in HTML
// Access metadata
$mimeType = $fileData->getMimeType(); // e.g., 'application/pdf'
$name = $fileData->getOriginalName(); // e.g., 'document.pdf'
$size = $fileData->getSize(); // Size in bytes
$formatted = $fileData->getFormattedSize(); // e.g., '1.50 MB'
// Utility methods
$extension = $fileData->getExtension(); // e.g., 'pdf'
$isImage = $fileData->isImage(); // true if image/* MIME type
$hash = $fileData->getHash(); // SHA-256 hash of content
// Save to file
$fileData->saveTo('/path/to/save.pdf');
// Create modified copy
$newFileData = $fileData->withMetadata('application/pdf', 'renamed.pdf');
The bundle supports two ways to store file metadata:
Metadata is stored in dedicated database columns, allowing you to:
#[EncryptedFile(
mimeTypeProperty: 'mimeType',
originalNameProperty: 'originalName',
originalSizeProperty: 'originalSize',
)]
private $content;
#[ORM\Column(type: Types::STRING, length: 100, nullable: true)]
private ?string $mimeType = null;
Metadata is stored inside the encrypted payload. Useful when you don't need to query by metadata.
#[EncryptedFile] // No metadata properties specified
private $content;
Metadata can still be extracted without full decryption using BinaryEncryptionService::extractMetadata().
Enable compression for text-based files to reduce storage:
#[EncryptedFile(compress: true)]
private $content;
Best practices:
Binary encrypted data uses the following format:
[magic: 4 bytes "CEFF"] - Identifies encrypted file format
[format_version: 1 byte] - Format version (currently 1)
[key_version: 1 byte] - Which key version was used
[flags: 1 byte] - Bit flags (bit 0 = compressed)
[metadata_length: 2 bytes] - Length of metadata JSON
[metadata: variable] - JSON with mimeType, originalName, originalSize
[iv: 12 bytes] - Initialization vector
[tag: 16 bytes] - GCM authentication tag
[encrypted_content: variable] - The encrypted file data
This format allows:
| Setting | Default | Maximum |
|---|---|---|
maxSize |
5 MB | 50 MB |
chunkSize |
160 KB | - |
Configure globally or per-field:
# config/packages/field_encryption.yaml
field_encryption:
file_encryption:
max_size: 10485760 # 10MB
chunk_size: 163840 # 160KB
compression: false
// Per-field override
#[EncryptedFile(maxSize: 20971520)] // 20MB for this field
private $largeDocument;
For simpler use cases where you don't need the DTO:
#[ORM\Column(type: Types::BLOB, nullable: true)]
#[EncryptedFile(plainType: 'string')]
private $content;
private ?string $plainContent = null; // Raw binary string
public function getPlainContent(): ?string
{
return $this->plainContent;
}
public function setPlainContent(?string $content): self
{
$this->plainContent = $content;
return $this;
}
#[Route('/document/{id}/download')]
public function download(Document $document): Response
{
$fileData = $document->getPlainContent();
if (!$fileData) {
throw $this->createNotFoundException();
}
$response = new Response($fileData->getContent());
$response->headers->set('Content-Type', $fileData->getMimeType());
$response->headers->set(
'Content-Disposition',
'attachment; filename="' . $fileData->getOriginalName() . '"'
);
return $response;
}
#[Route('/document/{id}/preview')]
public function preview(Document $document): Response
{
$fileData = $document->getPlainContent();
if (!$fileData || !$fileData->isImage()) {
throw $this->createNotFoundException();
}
// Return as inline image
return new Response($fileData->getContent(), 200, [
'Content-Type' => $fileData->getMimeType(),
]);
}
{# Embed image directly #}
<img src="{{ document.plainContent.toDataUri() }}" alt="{{ document.originalName }}">
{# Display file info #}
<p>
File: {{ document.originalName }}<br>
Type: {{ document.mimeType }}<br>
Size: {{ document.plainContent.formattedSize }}
</p>
How can I help you explore Laravel packages today?