Database encryption at PHP Symfony Web App

D

In the current digital landscape, especially taking into consideration new law requirements set by General Data Protection Regulation (GDPR), the security of sensitive data is of utmost importance. This article provides a comprehensive overview on how to implement database encryption within web application and what types of db encryption can we implement as software engineers. As whole current material is closely related with PHP+Symfony context, it also provides some practical examples for Symfony framework. So, let’s start.

Database encryption can mainly be classified into two categories:

1. Transparent Data Encryption (TDE)

  • TDE applies an additional security layer to protect stored data from offline attacks such as unauthorized file accesses or backups. This is particularly useful in scenarios like data center theft or improper disposal of storage devices.
  • TDE encryption is almost a standard at any well known cloud, as example you can turn on it at AWS RDS using only one parameter at terraform side.

2. Always Encrypted

  • This feature safeguards sensitive data from database administrators or malicious actors trying to access it via SQL injection attacks. The data remains secure even if the server itself is compromised.
  • Always Encrypted uses client-side encryption, ensuring data is encrypted before being sent to the database and decrypted when retrieved.

To apply “Always Encrypted” DB encryption in a Symfony-based PHP application, we can consider the following approaches:

1. Symmetric Encryption with a Master Key:

  • Utilize a Symfony bundle such as Mogilvie’s EncryptBundle.
  • Alternatively, implement a custom encrypted field at the ORM level, which would be described in details below

2. Asymmetric Encryption with Individual Keys for Each User:

  • This approach is more prevalent in sectors requiring super high security, like banking and healthcare. Each user has a separate encryption key, ensuring data is inaccessible to other users.

The asymmetric db encryption involves a lot of complexity and it is very specific. What is much more simple and can be used at broad scale is symmetric approach, which would be discussed in details further.

Steps to Implement Symmetric Encryption

1. Identify Data to Encrypt:

  • Determine the critical data needing encryption, such as email addresses, tax IDs, company addresses, and phone numbers.

2. Check Data Usage in Joins:

  • Ensure that none of the identified data is used in database joins. If it is, queries need to be adjusted to utilize unique identifiers.

3. Develop Encryption/Decryption Scripts:

  • Create scripts that encrypt existing data and decrypt it when necessary.

4. Key Rotation Policy:

  • Set an expiration policy for the master encryption key (e.g., every two years). When the key expires, decrypt the data using the old key and re-encrypt it with a new one.

5. Performance Testing:

  • Implement and test the changes in a staging environment to check performance impacts and ensure the system can handle the key rotation process.

6. Deploy to Production:

  • If the performance metrics are acceptable, deploy the “Always Encrypted” setup using symmetric encryption in the production environment.

Implementing Encryption in Symfony

To automate encryption in Symfony 6th, 7th version, without manual intervention for each operation, we can use a custom Doctrine type to handle encryption and decryption seamlessly.

Step 1: Create an Encryption Service

Define an encryption interface:

interface EncryptionServiceInterface
{
    public function encrypt(string $plainText): string;
    public function decrypt(string $cipherText): string;
}

Now, let’s use OpenSSL for the implementation:

// src/Service/OpenSslEncryptionService.php
namespace App\Service;

class OpenSslEncryptionService implements EncryptionServiceInterface
{
    const SSL_OPTIONS = 0;

    private string $cipherMethod;
    private string $encryptionKey;
    private string $initializationVector;

    public function __construct(
        string $cipherMethod = 'AES-128-CTR',
        string $encryptionKey = 'EncryptionKey',
        string $initializationVector = '1234567891011121'
    )
    {
        $this->cipherMethod = $cipherMethod;
        $this->encryptionKey = $encryptionKey;
        $this->initializationVector = $initializationVector;
    }
    
    public function encrypt(string $plainText): string
    {
        return openssl_encrypt(
            $plainText,
            $this->cipherMethod,
            $this->encryptionKey,
            self::SSL_OPTIONS,
            $this->initializationVector
        );
    }

    public function decrypt(string $cipherText): string
    {
        return openssl_decrypt(
            $cipherText,
            $this->cipherMethod,
            $this->encryptionKey,
            self::SSL_OPTIONS,
            $this->initializationVector
        );
    }
}

Step 2: Create a Custom Doctrine Type

Define a new EncryptedType class:

// src/Doctrine/Types/EncryptedType.php
namespace App\Doctrine\Types;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use App\Service\EncryptionServiceInterface;

class EncryptedType extends Type
{
    const ENCRYPTED = 'encrypted';
    private EncryptionServiceInterface $encryptionService;

    public function setEncryptionService(EncryptionServiceInterface $encryptionService): void
    {
        $this->encryptionService = $encryptionService;
    }

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
    {
        return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): string
    {
        return $this->encryptionService->encrypt($value);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): string
    {
        return $this->encryptionService->decrypt($value);
    }

    public function getName(): string
    {
        return self::ENCRYPTED;
    }
}

Step 3: Register Services and Custom Type

Add the encryption service configuration in config/services.yaml.

services:
    App\Service\EncryptionServiceInterface: '@App\Service\OpenSslEncryptionService'
    App\Service\OpenSslEncryptionService:
        arguments:
            $encryptionKey: '%env(ENCRYPTION_KEY)%'
            $initializationVector: '%env(INITIALIZATION_VECTOR)%'

Create a new file src/DependencyInjection/Compiler/DoctrineEncryptionCompilerPass.php:

namespace App\DependencyInjection\Compiler;

use App\Doctrine\Types\EncryptedType;
use App\Service\EncryptionServiceInterface;
use Doctrine\DBAL\Types\Type;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class DoctrineEncryptionCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!Type::hasType(EncryptedType::ENCRYPTED)) {
            $encryptionService = $container->get(EncryptionServiceInterface::class);
            Type::addType(EncryptedType::ENCRYPTED, EncryptedType::class);
            $encryptedType = Type::getType(EncryptedType::ENCRYPTED);
            $encryptedType->setEncryptionService($encryptionService);
        }
    }
}

Modify the src/Kernel.php to register your compiler pass.

namespace App;

use App\DependencyInjection\Compiler\DoctrineEncryptionCompilerPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    protected function build(ContainerBuilder $container): void
    {
        parent::build($container);
        $container->addCompilerPass(new DoctrineEncryptionCompilerPass());
    }
}

Step 4: Use the Custom Doctrine Type

Define your entity using the custom field type:

// src/Entity/YourEntity.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use App\Doctrine\Types\EncryptedType;

/**
 * @ORM\Entity()
 */
class YourEntity
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private int $id;

    /**
     * @ORM\Column(type=EncryptedType::ENCRYPTED)
     */
    private string $sensitiveData;
}

Please, take into consideration that some parts of realization may be not fully 100% working and will require some additional modifications at your concrete version of Symfony framework. Anyway, implementing database encryption in your Symfony application, especially using Doctrine, can be straightforward if approached correctly. By following these steps, you can ensure that your sensitive data remains secure without compromising the ease of management and performance of your application.

In case you are interested more at cyber security for web applications, then welcome to my course: “DevSecOps: How to secure web application with AWS WAF and CloudWatch“. Here you may find coupon with discount.

architecture AWS cluster cyber-security devops devops-basics docker elasticsearch flask geo high availability java machine learning opensearch php programming languages python recommendation systems search systems spring boot symfony