Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/example-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Example App

on:
push:
branches: [ main, example-app ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_DB: test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v3

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: pgsql, pdo_pgsql

- name: Install PostgreSQL client
run: sudo apt-get update && sudo apt-get install -y postgresql-client

- name: Install dependencies
run: composer install --working-dir=docs/example --prefer-dist --no-progress

- name: Create test_test database
env:
PGPASSWORD: ""
run: createdb -h localhost -p 5432 -U test test_test

- name: Run tests
working-directory: docs/example
run: ./vendor/bin/phpunit
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Doctrine CipherSweet Adapter

[![Build Status](https://github.com/paragonie/doctrine-ciphersweet/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/doctrine-ciphersweet/actions)
[![Example App](https://github.com/paragonie/doctrine-ciphersweet/actions/workflows/example-app.yml/badge.svg)](https://github.com/paragonie/doctrine-ciphersweet/tree/main/docs/example-app)
[![Static Analysis](https://github.com/paragonie/doctrine-ciphersweet/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/doctrine-ciphersweet/actions)
[![Latest Stable Version](https://poser.pugx.org/paragonie/doctrine-ciphersweet/v/stable)](https://packagist.org/packages/paragonie/doctrine-cipher)
[![Latest Unstable Version](https://poser.pugx.org/paragonie/doctrine-ciphersweet/v/unstable)](https://packagist.org/packages/paragonie/doctrine-cipher)
Expand Down
212 changes: 212 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Using the CipherSweet adapter for Doctrine

This guide will walk you through using the adapter in your Doctrine-based apps.

## Installation

```bash
composer require paragonie/doctrine-ciphersweet
```

## Configuration

First, you need a `ParagonIE\CipherSweet\CipherSweet` object. Please refer to
[the CipherSweet docs](https://ciphersweet.paragonie.com/php/setup) for more information.

```php
use ParagonIE\CipherSweet\CipherSweet;
use ParagonIE\CipherSweet\KeyProvider\StringProvider;

$keyProvider = new StringProvider(random_bytes(32));
$engine = new CipherSweet($keyProvider);
```

Next, create an `EncryptedFieldSubscriber` and register it with your `EntityManager`.

```php
use ParagonIE\DoctrineCipher\Event\EncryptedFieldSubscriber;

$subscriber = new EncryptedFieldSubscriber($engine);
$entityManager->getEventManager()->addEventSubscriber($subscriber);
```

### Symfony Configuration

If you're using Symfony, you can configure the subscriber in your `services.yaml` file.

First, make sure you have a `CIPHERSWEET_KEY` environment variable defined in your `.env` file.
It must be a 64-character hexadecimal string.

```env
# .env
CIPHERSWEET_KEY=your-64-character-hexadecimal-key
```

Then, configure the services in `config/services.yaml`:

```yaml
# config/services.yaml
parameters:
env(CIPHERSWEET_KEY): ''

services:
ParagonIE\CipherSweet\KeyProvider\StringProvider:
factory: ['App\Factory\CipherSweetKeyProviderFactory', 'create']
arguments:
- '%env(CIPHERSWEET_KEY)%'

ParagonIE\CipherSweet\CipherSweet:
arguments:
- '@ParagonIE\CipherSweet\KeyProvider\StringProvider'

ParagonIE\DoctrineCipher\Event\EncryptedFieldSubscriber:
arguments:
- '@ParagonIE\CipherSweet\CipherSweet'
tags:
- { name: doctrine.event_subscriber, connection: default }
```

You will also need to create a factory to create the `StringProvider` from the hexadecimal key
in your `.env` file.

```php
// src/Factory/CipherSweetKeyProviderFactory.php
<?php
declare(strict_types=1);
namespace App\Factory;

use ParagonIE\CipherSweet\KeyProvider\StringProvider;

final class CipherSweetKeyProviderFactory
{
public static function create(string $key): StringProvider
{
return new StringProvider(hex2bin($key));
}
}
```

## Usage

Once the above steps are complete, you can use the `#[Encrypted]` attribute on your entity properties.

```php
use Doctrine\ORM\Mapping as ORM;
use ParagonIE\DoctrineCipher\Attribute\Encrypted;

#[ORM\Entity]
class Message
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private int $id;

#[ORM\Column(type: 'text')]
#[Encrypted]
private string $text;

public function __construct(string $text)
{
$this->text = $text;
}

// ... getters and setters
}
```

When you persist an entity, the `EncryptedFieldSubscriber` will automatically encrypt the properties that have the
`#[Encrypted]` attribute.

```php
$message = new Message('This is a secret message.');
$entityManager->persist($message);
$entityManager->flush();
```

When you retrieve an entity, the encrypted properties will be automatically decrypted.

```php
$message = $entityManager->find(Message::class, 1);
echo $message->getText(); // "This is a secret message."
```

### Blind Indexes

You can also use blind indexes for searchable encryption. To do this, add a `blindIndexes` argument to the
`#[Encrypted]` attribute.

```php
use Doctrine\ORM\Mapping as ORM;
use ParagonIE\DoctrineCipher\Attribute\Encrypted;

#[ORM\Entity]
class Message
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private int $id;

#[ORM\Column(type: 'text')]
#[Encrypted(blindIndexes: ['insensitive' => 'case-insensitive'])]
private string $text;

#[ORM\Column(type: 'string', length: 255, nullable: true)]
private ?string $textBlindIndexInsensitive;

public function __construct(string $text)
{
$this->text = $text;
}

// ... getters and setters
}
```

Observe the attribute: `#[Encrypted(blindIndexes: ['insensitive' => 'case-insensitive'])]`.

In order for this to succeed, you need to register a transformer for the blind index.

```php
use ParagonIE\CipherSweet\Transformation\Lowercase;

$subscriber->addTransformer('case-insensitive', Lowercase::class);
```

If you're using Symfony, you can add the transformer to your `services.yaml` file.

```yaml
# config/services.yaml
services:
ParagonIE\DoctrineCipher\Event\EncryptedFieldSubscriber:
# ...
calls:
- ['addTransformer', ['case-insensitive', 'ParagonIE\CipherSweet\Transformation\Lowercase']]
```

## Complete Example

Now you can query the blind index. To do so, you must first calculate the blind index for your search term.

```php
use ParagonIE\CipherSweet\BlindIndex;
use ParagonIE\CipherSweet\EncryptedField;

// First, you need to get the blind index for your search term.
// Note: The EncryptedField must be configured exactly as it is for the entity.
$encryptedField = new EncryptedField($engine, 'messages', 'text');
$encryptedField->addBlindIndex(new BlindIndex('insensitive', [new Lowercase()]));

$searchTerm = 'this is a secret message.';
$blindIndex = $encryptedField->getBlindIndex($searchTerm, 'insensitive');

// Now you can use this blind index to query the database.
$repository = $entityManager->getRepository(Message::class);
$message = $repository->findOneBy(['textBlindIndexInsensitive' => $blindIndex]);
```

## Example App

The [example](example) directory contains an example Symfony application that uses the Doctrine-CipherSweet adapter.
This example app is tested as part of our CI/CD pipeline, so the code there is guaranteed to work if the build passes.
17 changes: 17 additions & 0 deletions docs/example/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[{compose.yaml,compose.*.yaml}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
38 changes: 38 additions & 0 deletions docs/example/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration

###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
###< symfony/framework-bundle ###

###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
CIPHERSWEET_KEY="2162a5508a829bf6888a7b321c436d6e143525b653e88691b8d2288062534da2"

###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
4 changes: 4 additions & 0 deletions docs/example/.env.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

###> symfony/framework-bundle ###
APP_SECRET=bded8759ccede8b520551b62e1cdabde
###< symfony/framework-bundle ###
3 changes: 3 additions & 0 deletions docs/example/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='64bd1c1127fb74c7aa304399689df2433bb4b3bd7004ac99468ca8cefe4beeac'
17 changes: 17 additions & 0 deletions docs/example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/composer.lock

###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/symfony.lock
/var/
/vendor/
###< symfony/framework-bundle ###

###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.cache/
###< phpunit/phpunit ###
19 changes: 19 additions & 0 deletions docs/example/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
21 changes: 21 additions & 0 deletions docs/example/bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php

use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;

if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}

if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);

return new Application($kernel);
};
Loading
Loading