Keeping data protected and safe is key in modern Enterprise Software Applications. In PHP we have available the openSSL extension to be bundled with it1. When developing Symfony Applications you can follow this simple recipe to allow required data to be saved in encrypted format, and then decrypt it when need it:

 

 Step 1. Make sure your Application has the openSSL extension declaring it as part your composer.json requirements:

#composer.json
{
    "type": "project",
    "license": "proprietary",
    "minimum-stability": "stable",
    "prefer-stable": true,
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-curl": "*",
        "ext-iconv": "*",
        "ext-json": "*",
        "ext-openssl": "*",
        "ext-simplexml": "*",
        "ext-tokenizer": "*",
        "ext-xmlwriter": "*",
        "ext-zlib": "*",
...

Step 2. Make a new class file where you are going to put the encryption methods:

namespace App\ApiModule\Services;


use App\ApiModule\Exceptions\SystemException;
use Exception;

/**
 * Class PiiCryptoService
 */
class PiiCryptoService
{
    const PII_KEY = 'some-alpha-num-with-special-chars-key';//This key should be passed as an ENV var and be kept in secret vault
    const CIPHERING = "AES-128-CBC";

    /**
     * @var false|int
     */
    private $iv_length;

    /**
     * @var false|string
     */
    private $encryption_key;

    /**
     * @param string $data
     * @return string
     */
    public function encryptData(string $data): string
    {
        $ivLen = openssl_cipher_iv_length(self::CIPHERING);
        $iv = openssl_random_pseudo_bytes($ivLen);
        $ciphertext_raw = openssl_encrypt($data, self::CIPHERING, self::PII_KEY, $options=OPENSSL_RAW_DATA, $iv);
        $hmac = hash_hmac('sha256', $ciphertext_raw, self::PII_KEY, $as_binary=true);
        return base64_encode( $iv.$hmac.$ciphertext_raw );
    }

    /**
     * @param string $encodedData
     * @return string
     */
    public function decryptData(string $encodedData): string
    {
        $c = base64_decode($encodedData);
        $ivLen = openssl_cipher_iv_length(self::CIPHERING);
        $iv = substr($c, 0, $ivLen);
        $hmac = substr($c, $ivLen, $sha2len=32);
        $ciphertext_raw = substr($c, $ivLen+$sha2len);
        return openssl_decrypt($ciphertext_raw, self::CIPHERING, self::PII_KEY, $options=OPENSSL_RAW_DATA, $iv);
    }

    /**
     * @param string $data
     * @return string
     */
    public function hashData(string $data): string
    {
        $hexString = unpack('H*', $data);
        $hex = array_shift($hexString);

        return base64_encode($hex);
    }

    /**
     * @param string $encodedData
     * @return string
     */
    public function unHashData(string $encodedData): string
    {
        return hex2bin(base64_decode($encodedData));
    }

}

The cipher algorithm can be this or you may want to use a different one. For more information use the link at the bottom to go to the reference documentation in PHP official website.

It is strongly recommended that the encryption Key is passed as an env variable and kept in a secret vault out of reach and where you are sure is not going to get lost. If you loose this key, all encrypted data will be lost.

Step 3. All Doctrine Entity files that will require data encryption must extend this class.

namespace App\ApiModule\Entity;

use App\ApiModule\Services\PiiCryptoService;
use Doctrine\ORM\Mapping as ORM;

/**
 * ContactInfo
 *
 * @ORM\Table(name="customers", uniqueConstraints={@ORM\UniqueConstraint(name="IDX_UNIQUE_PRIMARY_EMAIL", columns={"primary_email"})})
 * @ORM\Entity(repositoryClass="App\ApiModule\Repository\CustomersRepository")
 */
class Customers extends PiiCryptoService
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;
    
    /**
     * @var string
     * @ORM\Column(name="first_name", type="string", length=255, nullable=false)
     */
    private string $firstName;
    
    /**
     * @var string
     * @ORM\Column(name="last_name", type="string", length=255, nullable=false)
     */
    private string $lastName;

    /**
     * @var string
     *
     * @ORM\Column(name="primary_email", type="string", length=255, nullable=false)
     */
    private $primaryEmail;
    
    /**
     * @var string
     *
     * @ORM\Column(name="phone_number", type="string", length=255, nullable=false)
     */
    private $phoneNumber;
    
    /**
     * @var string
     *
     * @ORM\Column(name="address", type="string", length=255, nullable=false)
     */
    private $address;

Notice that all columns that will store encrypted data need to have a bigger length than usual.

Step 4. For all the fields that need to be encrypted, do this in the getter and setter:

    
    /**
     * @return string
     */
    public function getFirstName(): string
    {
        return $this->decryptData($this->firstName);
    }
    
    /**
     * @param string $firstName
     * @return $this
     */
    public function setFirstName(string $firstName): self
    {
        $this->firstName = $this->encryptData($firstName);

        return $this;
    }

 

That's it!. Now from everywhere you Instantiate this Entity object, you can get or set the data as usual, without caring about encryption.

Data in the DB will be encrypted, thus if browsing at it with SQL Client like MySQL Workbench3   will show something like this:

 encrypted-customers-in-database

 

Step 5. If you need to query data filtering by an encrypted field, you need to use the hashing functions provided in the PiiCryptoService Class instead in the getter and setter:

    
    /**
     * @return string
     */
    public function getPrimaryEmail(): string
    {
        return $this->unHashData($this->primaryEmail);
    }
    
    /**
     * @param string $primaryEmail
     * @return $this
     */
    public function setPrimaryEmail(string $primaryEmail): self
    {
        $this->primaryEmail = $this->hashData($primaryEmail);

        return $this;
    }

Then implement something like this:

namespace App\ApiModule\Repository;

use App\ApiModule\Entity\Customers;
use App\ApiModule\Services\PiiCryptoService;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @method Customers|null find($id, $lockMode = null, $lockVersion = null)
 * @method Customers|null findOneBy(array $criteria, array $orderBy = null)
 * @method Customers[]    findAll()
 * @method Customers[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class CustomersRepository extends ServiceEntityRepository
{
    /**
     * @var PiiCryptoService 
     */
    private PiiCryptoService $piiCryptoService;
    
    /**
     * CustomersRepository constructor.
     * @param ManagerRegistry $registry
     */
    public function __construct(ManagerRegistry $registry, PiiCryptoService $piiCryptoService)
    {
        parent::__construct($registry, Customers::class);
        $this->piiCryptoService = $piiCryptoService;
    }
    
    /**
     * @param string $email
     * @return float|int|mixed|string|null
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function getCustomerByEmail(string $email)
    {
        $entityManager = $this->getEntityManager();
        
        $sql = 'SELECT c
            FROM App\ApiModule\Entity\Customers c
            WHERE c.primaryEmail = :email';

        $query = $entityManager->createQuery($sql)->setParameter('email', $this->piiCryptoService->hashData($email));

       
        return $query->getOneOrNullResult();
    }
    
}

 As the data in the DB is encrypted, you need to run the query providing the encrypted version of the data you want to filter in the WHERE clause. In this case, the hashing method is applied to the email string as when the Entity was saved, this field was hashed. Also, the result will be in encrypted mode, so you need to to use the getters in the Entity to get the decrypted data:

App\ApiModule\Entity\Customers {#1598 ▼
  -iv_length: null
  -encryption_key: null
  -id: 123
  -firstName: "oPjOxCjgOHNwCpq4dUq5M+wvf6HkUOMbiN7Yh1nPeiOjuE26LoKiVLDyhCzC8eNviM5VrIzZbb71yIfJH/JdGw=="
  -lastName: "EbXu8SSjQTVMjveENNxoExBYyny+FU3oLg5NVqE0sboNQsJeik2e06ETBScg0LiCiuUGiI2O2S7QQovhRzY68w=="
  -primaryEmail: "lGMsfqhEPzOg0uUjqajrgrVurk46+rPeDc8OMi6u4P1gBiblePy7wJoGqALDCyVg/FZEyk0b2ZXo8ESvGAO0kKLbe7932tjOPrGeH3plqVXdvQLBsxix2guTxXt5qr7E"
  -phoneNumber: "WSH7nIsD79+65NdKoIW7XYXui/ba/NghUrmkxnkl+Sm6nstTINZ8oO9ZwKs9vcoZXki5cOM2TNfi6RLS3c8eHDzFQfKB/HrI4IvflYgLdkE="
  -address: "48529 jovanny stream apt. 912"
  -zipCode: Proxies\__CG__\App\ApiModule\Entity\ZipCodes {#1647 ▼
    -id: 10576
    -zipCode: null
    -cities: null
    +__isInitialized__: false
     …2
  }
}

 For doing this you may want to implement a Transformer class and use DTOs2 to return decrypted Data in a consistent way:

{
  "id": 123,
  "firstName": "nikki",
  "lastName": "koh",
  "primaryEmail": "nikki.koh@fake_email.wintheiser.com",
  "phoneNumber": "(908)314-0728x07968",
  "address": "48529 jovanny stream apt. 912",
  "zipCode": {
    "id": 10576,
    "zipCode": "54725",
    "city": {
      "id": 11658,
      "cityName": "boyceville",
      "state": {
        "id": 49,
        "stateName": "wisconsin",
        "stateShortName": "WI",
        "country": {
          "id": 1,
          "countryName": "United States"
        }
      }
    }
  }
}

 

 

1 Reference: PHP OpenSSL

2 Read more on DTOs here: Using DTOs in PHP Symfony

3 Using MySql Workbench to model and create a Database Schema with Tables