Skip to content

Commit 82714e6

Browse files
Merge pull request #1 from paragonie/example-app
Documentation and Example App
2 parents 012bf70 + cb97865 commit 82714e6

33 files changed

+966
-0
lines changed

.github/workflows/example-app.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Example App
2+
3+
on:
4+
push:
5+
branches: [ main, example-app ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
services:
14+
postgres:
15+
image: postgres:16-alpine
16+
env:
17+
POSTGRES_USER: test
18+
POSTGRES_DB: test
19+
POSTGRES_HOST_AUTH_METHOD: trust
20+
ports:
21+
- 5432:5432
22+
options: >-
23+
--health-cmd pg_isready
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
28+
steps:
29+
- uses: actions/checkout@v3
30+
31+
- name: Set up PHP
32+
uses: shivammathur/setup-php@v2
33+
with:
34+
php-version: '8.2'
35+
extensions: pgsql, pdo_pgsql
36+
37+
- name: Install PostgreSQL client
38+
run: sudo apt-get update && sudo apt-get install -y postgresql-client
39+
40+
- name: Install dependencies
41+
run: composer install --working-dir=docs/example --prefer-dist --no-progress
42+
43+
- name: Create test_test database
44+
env:
45+
PGPASSWORD: ""
46+
run: createdb -h localhost -p 5432 -U test test_test
47+
48+
- name: Run tests
49+
working-directory: docs/example
50+
run: ./vendor/bin/phpunit

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Doctrine CipherSweet Adapter
22

33
[![Build Status](https://github.com/paragonie/doctrine-ciphersweet/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/doctrine-ciphersweet/actions)
4+
[![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)
45
[![Static Analysis](https://github.com/paragonie/doctrine-ciphersweet/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/doctrine-ciphersweet/actions)
56
[![Latest Stable Version](https://poser.pugx.org/paragonie/doctrine-ciphersweet/v/stable)](https://packagist.org/packages/paragonie/doctrine-cipher)
67
[![Latest Unstable Version](https://poser.pugx.org/paragonie/doctrine-ciphersweet/v/unstable)](https://packagist.org/packages/paragonie/doctrine-cipher)

docs/README.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Using the CipherSweet adapter for Doctrine
2+
3+
This guide will walk you through using the adapter in your Doctrine-based apps.
4+
5+
## Installation
6+
7+
```bash
8+
composer require paragonie/doctrine-ciphersweet
9+
```
10+
11+
## Configuration
12+
13+
First, you need a `ParagonIE\CipherSweet\CipherSweet` object. Please refer to
14+
[the CipherSweet docs](https://ciphersweet.paragonie.com/php/setup) for more information.
15+
16+
```php
17+
use ParagonIE\CipherSweet\CipherSweet;
18+
use ParagonIE\CipherSweet\KeyProvider\StringProvider;
19+
20+
$keyProvider = new StringProvider(random_bytes(32));
21+
$engine = new CipherSweet($keyProvider);
22+
```
23+
24+
Next, create an `EncryptedFieldSubscriber` and register it with your `EntityManager`.
25+
26+
```php
27+
use ParagonIE\DoctrineCipher\Event\EncryptedFieldSubscriber;
28+
29+
$subscriber = new EncryptedFieldSubscriber($engine);
30+
$entityManager->getEventManager()->addEventSubscriber($subscriber);
31+
```
32+
33+
### Symfony Configuration
34+
35+
If you're using Symfony, you can configure the subscriber in your `services.yaml` file.
36+
37+
First, make sure you have a `CIPHERSWEET_KEY` environment variable defined in your `.env` file.
38+
It must be a 64-character hexadecimal string.
39+
40+
```env
41+
# .env
42+
CIPHERSWEET_KEY=your-64-character-hexadecimal-key
43+
```
44+
45+
Then, configure the services in `config/services.yaml`:
46+
47+
```yaml
48+
# config/services.yaml
49+
parameters:
50+
env(CIPHERSWEET_KEY): ''
51+
52+
services:
53+
ParagonIE\CipherSweet\KeyProvider\StringProvider:
54+
factory: ['App\Factory\CipherSweetKeyProviderFactory', 'create']
55+
arguments:
56+
- '%env(CIPHERSWEET_KEY)%'
57+
58+
ParagonIE\CipherSweet\CipherSweet:
59+
arguments:
60+
- '@ParagonIE\CipherSweet\KeyProvider\StringProvider'
61+
62+
ParagonIE\DoctrineCipher\Event\EncryptedFieldSubscriber:
63+
arguments:
64+
- '@ParagonIE\CipherSweet\CipherSweet'
65+
tags:
66+
- { name: doctrine.event_subscriber, connection: default }
67+
```
68+
69+
You will also need to create a factory to create the `StringProvider` from the hexadecimal key
70+
in your `.env` file.
71+
72+
```php
73+
// src/Factory/CipherSweetKeyProviderFactory.php
74+
<?php
75+
declare(strict_types=1);
76+
namespace App\Factory;
77+
78+
use ParagonIE\CipherSweet\KeyProvider\StringProvider;
79+
80+
final class CipherSweetKeyProviderFactory
81+
{
82+
public static function create(string $key): StringProvider
83+
{
84+
return new StringProvider(hex2bin($key));
85+
}
86+
}
87+
```
88+
89+
## Usage
90+
91+
Once the above steps are complete, you can use the `#[Encrypted]` attribute on your entity properties.
92+
93+
```php
94+
use Doctrine\ORM\Mapping as ORM;
95+
use ParagonIE\DoctrineCipher\Attribute\Encrypted;
96+
97+
#[ORM\Entity]
98+
class Message
99+
{
100+
#[ORM\Id]
101+
#[ORM\Column(type: 'integer')]
102+
#[ORM\GeneratedValue]
103+
private int $id;
104+
105+
#[ORM\Column(type: 'text')]
106+
#[Encrypted]
107+
private string $text;
108+
109+
public function __construct(string $text)
110+
{
111+
$this->text = $text;
112+
}
113+
114+
// ... getters and setters
115+
}
116+
```
117+
118+
When you persist an entity, the `EncryptedFieldSubscriber` will automatically encrypt the properties that have the
119+
`#[Encrypted]` attribute.
120+
121+
```php
122+
$message = new Message('This is a secret message.');
123+
$entityManager->persist($message);
124+
$entityManager->flush();
125+
```
126+
127+
When you retrieve an entity, the encrypted properties will be automatically decrypted.
128+
129+
```php
130+
$message = $entityManager->find(Message::class, 1);
131+
echo $message->getText(); // "This is a secret message."
132+
```
133+
134+
### Blind Indexes
135+
136+
You can also use blind indexes for searchable encryption. To do this, add a `blindIndexes` argument to the
137+
`#[Encrypted]` attribute.
138+
139+
```php
140+
use Doctrine\ORM\Mapping as ORM;
141+
use ParagonIE\DoctrineCipher\Attribute\Encrypted;
142+
143+
#[ORM\Entity]
144+
class Message
145+
{
146+
#[ORM\Id]
147+
#[ORM\Column(type: 'integer')]
148+
#[ORM\GeneratedValue]
149+
private int $id;
150+
151+
#[ORM\Column(type: 'text')]
152+
#[Encrypted(blindIndexes: ['insensitive' => 'case-insensitive'])]
153+
private string $text;
154+
155+
#[ORM\Column(type: 'string', length: 255, nullable: true)]
156+
private ?string $textBlindIndexInsensitive;
157+
158+
public function __construct(string $text)
159+
{
160+
$this->text = $text;
161+
}
162+
163+
// ... getters and setters
164+
}
165+
```
166+
167+
Observe the attribute: `#[Encrypted(blindIndexes: ['insensitive' => 'case-insensitive'])]`.
168+
169+
In order for this to succeed, you need to register a transformer for the blind index.
170+
171+
```php
172+
use ParagonIE\CipherSweet\Transformation\Lowercase;
173+
174+
$subscriber->addTransformer('case-insensitive', Lowercase::class);
175+
```
176+
177+
If you're using Symfony, you can add the transformer to your `services.yaml` file.
178+
179+
```yaml
180+
# config/services.yaml
181+
services:
182+
ParagonIE\DoctrineCipher\Event\EncryptedFieldSubscriber:
183+
# ...
184+
calls:
185+
- ['addTransformer', ['case-insensitive', 'ParagonIE\CipherSweet\Transformation\Lowercase']]
186+
```
187+
188+
## Complete Example
189+
190+
Now you can query the blind index. To do so, you must first calculate the blind index for your search term.
191+
192+
```php
193+
use ParagonIE\CipherSweet\BlindIndex;
194+
use ParagonIE\CipherSweet\EncryptedField;
195+
196+
// First, you need to get the blind index for your search term.
197+
// Note: The EncryptedField must be configured exactly as it is for the entity.
198+
$encryptedField = new EncryptedField($engine, 'messages', 'text');
199+
$encryptedField->addBlindIndex(new BlindIndex('insensitive', [new Lowercase()]));
200+
201+
$searchTerm = 'this is a secret message.';
202+
$blindIndex = $encryptedField->getBlindIndex($searchTerm, 'insensitive');
203+
204+
// Now you can use this blind index to query the database.
205+
$repository = $entityManager->getRepository(Message::class);
206+
$message = $repository->findOneBy(['textBlindIndexInsensitive' => $blindIndex]);
207+
```
208+
209+
## Example App
210+
211+
The [example](example) directory contains an example Symfony application that uses the Doctrine-CipherSweet adapter.
212+
This example app is tested as part of our CI/CD pipeline, so the code there is guaranteed to work if the build passes.

docs/example/.editorconfig

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
charset = utf-8
7+
end_of_line = lf
8+
indent_size = 4
9+
indent_style = space
10+
insert_final_newline = true
11+
trim_trailing_whitespace = true
12+
13+
[{compose.yaml,compose.*.yaml}]
14+
indent_size = 2
15+
16+
[*.md]
17+
trim_trailing_whitespace = false

docs/example/.env

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# In all environments, the following files are loaded if they exist,
2+
# the latter taking precedence over the former:
3+
#
4+
# * .env contains default values for the environment variables needed by the app
5+
# * .env.local uncommitted file with local overrides
6+
# * .env.$APP_ENV committed environment-specific defaults
7+
# * .env.$APP_ENV.local uncommitted environment-specific overrides
8+
#
9+
# Real environment variables win over .env files.
10+
#
11+
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
12+
# https://symfony.com/doc/current/configuration/secrets.html
13+
#
14+
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
15+
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
16+
17+
###> symfony/framework-bundle ###
18+
APP_ENV=dev
19+
APP_SECRET=
20+
###< symfony/framework-bundle ###
21+
22+
###> symfony/routing ###
23+
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
24+
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
25+
DEFAULT_URI=http://localhost
26+
###< symfony/routing ###
27+
CIPHERSWEET_KEY="2162a5508a829bf6888a7b321c436d6e143525b653e88691b8d2288062534da2"
28+
29+
###> doctrine/doctrine-bundle ###
30+
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
31+
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
32+
#
33+
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
34+
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
35+
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
36+
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
37+
###< doctrine/doctrine-bundle ###
38+
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

docs/example/.env.dev

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
###> symfony/framework-bundle ###
3+
APP_SECRET=bded8759ccede8b520551b62e1cdabde
4+
###< symfony/framework-bundle ###

docs/example/.env.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# define your env variables for the test env here
2+
KERNEL_CLASS='App\Kernel'
3+
APP_SECRET='64bd1c1127fb74c7aa304399689df2433bb4b3bd7004ac99468ca8cefe4beeac'

docs/example/.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/composer.lock
2+
3+
###> symfony/framework-bundle ###
4+
/.env.local
5+
/.env.local.php
6+
/.env.*.local
7+
/config/secrets/prod/prod.decrypt.private.php
8+
/public/bundles/
9+
/symfony.lock
10+
/var/
11+
/vendor/
12+
###< symfony/framework-bundle ###
13+
14+
###> phpunit/phpunit ###
15+
/phpunit.xml
16+
/.phpunit.cache/
17+
###< phpunit/phpunit ###

docs/example/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

docs/example/bin/console

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
use App\Kernel;
5+
use Symfony\Bundle\FrameworkBundle\Console\Application;
6+
7+
if (!is_dir(dirname(__DIR__).'/vendor')) {
8+
throw new LogicException('Dependencies are missing. Try running "composer install".');
9+
}
10+
11+
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
12+
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
13+
}
14+
15+
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
16+
17+
return function (array $context) {
18+
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
19+
20+
return new Application($kernel);
21+
};

0 commit comments

Comments
 (0)