Skip to content

Commit 2924214

Browse files
committed
Enhance documentation for MysqlAes encryptor
- Updated CONFIGURATION.md to clarify the use of the `<ENC>` marker in MySQL and its implications for data insertion and retrieval. - Expanded MYSQL_AES.md with detailed examples for inserting encrypted data using both Doctrine and native SQL, emphasizing the importance of column types and formats to avoid confusion.
1 parent 1e29cc7 commit 2924214

2 files changed

Lines changed: 231 additions & 2 deletions

File tree

docs/CONFIGURATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Each config has its own key file: `.{encryptor_class}.{alias}.key` (e.g. `.Halit
6969
```
7070

7171
- **MysqlAes**
72-
Compatible with MySQL [`AES_ENCRYPT()`](https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html) / [`AES_DECRYPT()`](https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html) (AES-128-ECB, default `block_encryption_mode`). The passphrase is a plain string (file or env). Use a **BLOB** column when encrypting in SQL. See [MYSQL_AES.md](MYSQL_AES.md) and the symfony8 demo (`/mysql-aes-note`). **Performance:** PHP OpenSSL on every ORM read/write; SQL `AES_ENCRYPT` only avoids PHP on insert/update for columns you manage in raw SQL. Searching secrets via `LIKE` on decrypted values in SQL is among the most expensive patterns — see [PERFORMANCE.md](PERFORMANCE.md).
72+
Compatible with MySQL [`AES_ENCRYPT()`](https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html) / [`AES_DECRYPT()`](https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html) (AES-128-ECB, default `block_encryption_mode`). The passphrase is a plain string (file or env). Use a **BLOB** column when encrypting in SQL. See [MYSQL_AES.md](MYSQL_AES.md) (INSERT examples, repository pattern, demo `/mysql-aes-note`) and the symfony8 demo routes under `/mysql-aes-note`. **Performance:** PHP OpenSSL on every ORM read/write; SQL `AES_ENCRYPT` only avoids PHP on insert/update for columns you manage in raw SQL. Searching secrets via `LIKE` on decrypted values in SQL is among the most expensive patterns — see [PERFORMANCE.md](PERFORMANCE.md).
7373

7474
### Encryptor and query performance (overview)
7575

docs/MYSQL_AES.md

Lines changed: 230 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,236 @@ private ?string $secret = null;
3030

3131
Doctrine will encrypt/decrypt in PHP (same algorithm as MySQL). The subscriber still appends the `<ENC>` marker for ORM-managed fields.
3232

33-
## Native SQL (repository / DQL)
33+
## The `<ENC>` marker — when you need it (and when you must not add it)
34+
35+
This is the most common source of confusion with **MysqlAes**.
36+
37+
| How you insert | Column (demo) | `<ENC>` in DB? | How to read |
38+
|----------------|---------------|----------------|-------------|
39+
| **`$em->persist()`** + `#[Encrypted('mysql_aes')]` | `secret_orm` | **Yes** (added by subscriber) | Doctrine load, `\|decrypt('mysql_aes')`, `EncryptUtil` |
40+
| **`EncryptUtil::encrypt($plain, 'mysql_aes')`** then save | `secret_orm` | **Yes** (util adds it) | Same as above |
41+
| **`INSERT … AES_ENCRYPT(…)`** in SQL | `secret_native` (BLOB) | **No** | `AES_DECRYPT` in SQL, or `MysqlAesEncryptor::decrypt($blob)` in PHP |
42+
| **`INSERT … CONCAT(AES_ENCRYPT(…), '<ENC>')`** (advanced) | `secret_orm` | **Yes** (you append in SQL) | Doctrine / `\|decrypt('mysql_aes')` — see below |
43+
44+
**Do not** append `<ENC>` to `secret_native` rows that you only read with `AES_DECRYPT`.
45+
**Do not** expect `AES_DECRYPT(secret_orm, …)` to work on Doctrine-managed columns (ciphertext differs and includes `<ENC>`).
46+
47+
### “I used `AES_ENCRYPT` and Doctrine / Twig does not decrypt”
48+
49+
That is **expected** if:
50+
51+
- You wrote to **`secret_orm`** without `<ENC>`, or
52+
- You wrote to **`secret_native`** but load the entity with Doctrine (subscriber looks for `<ENC>` on `secret_orm` only), or
53+
- You use `{{ value|decrypt }}` on a blob/hex string **without** the `<ENC>` suffix.
54+
55+
**Fix:** pick one read path per column (see table above). In the demo, use **`/mysql-aes-note/db-values`** to compare raw vs decrypted.
56+
57+
### Insert encrypted data **for Doctrine** (you do not build `<ENC>` yourself)
58+
59+
Let the bundle add the marker:
60+
61+
```php
62+
// Option A — entity (recommended)
63+
$note = new MysqlAesNote();
64+
$note->setTitle('Invoice');
65+
$note->setSecretOrm('my-secret'); // plaintext in PHP
66+
$em->persist($note);
67+
$em->flush(); // stored as: MysqlAes ciphertext + "<ENC>"
68+
69+
// Option B — EncryptUtil (e.g. before raw UPDATE)
70+
use Nowo\DoctrineEncryptBundle\Util\EncryptUtil;
71+
72+
$stored = $encryptUtil->encrypt('my-secret', 'mysql_aes');
73+
// $stored ends with "<ENC>" — safe to persist on an Encrypted property
74+
```
75+
76+
Twig (value must be the **raw DB string** including `<ENC>`, e.g. from SQL export):
77+
78+
```twig
79+
{{ row.secret_orm_raw|decrypt('mysql_aes') }}
80+
```
81+
82+
### Insert with **`AES_ENCRYPT` in SQL** (no `<ENC>`)
83+
84+
Use a **separate BLOB column** (demo: `secret_native`):
85+
86+
```sql
87+
INSERT INTO mysql_aes_note (title, secret_native)
88+
VALUES ('SQL row', AES_ENCRYPT('my-secret', 'your_mysql_aes_passphrase'));
89+
```
90+
91+
Read in SQL:
92+
93+
```sql
94+
SELECT CAST(AES_DECRYPT(secret_native, 'your_mysql_aes_passphrase') AS CHAR) AS plain
95+
FROM mysql_aes_note WHERE id = 1;
96+
```
97+
98+
Read in PHP (no `<ENC>`, no Twig `|decrypt`):
99+
100+
```php
101+
$plain = $encryptorRegistry->get('mysql_aes')->decrypt($binaryBlobFromDb);
102+
```
103+
104+
### Advanced: SQL `INSERT` into an ORM column **with** `<ENC>` (MySQL only)
105+
106+
Only if you **must** insert in SQL but read via Doctrine/`#[Encrypted('mysql_aes')]` on the **same** column. The ciphertext bytes from MySQL must match PHP `MysqlAes` (same passphrase, default `block_encryption_mode`).
107+
108+
```sql
109+
INSERT INTO mysql_aes_note (title, secret_orm)
110+
VALUES (
111+
'From SQL for ORM',
112+
CONCAT(AES_ENCRYPT('my-secret', :passphrase), '<ENC>')
113+
);
114+
```
115+
116+
Prefer **VARBINARY/BLOB** for binary-safe storage; the demo uses `TEXT` for `secret_orm` (works for typical UTF-8 secrets but binary in `TEXT` can be charset-sensitive).
117+
118+
After insert, `$em->find(MysqlAesNote::class, $id)` should decrypt on load.
119+
**Do not** use this on `secret_native` if you also use `AES_DECRYPT` there — choose one format per column.
120+
121+
## Inserting encrypted data in MySQL (native SQL)
122+
123+
Use this when the **ciphertext must be produced by MySQL** (`AES_ENCRYPT`), e.g. legacy data, ETL, or another service writing directly to the DB. The plaintext is **not** stored; only the binary result of `AES_ENCRYPT` goes into the column.
124+
125+
### 1. Table and column type
126+
127+
Store ciphertext in **BLOB** or **VARBINARY** (not plain `TEXT`/`VARCHAR`):
128+
129+
```sql
130+
CREATE TABLE my_secrets (
131+
id INT AUTO_INCREMENT PRIMARY KEY,
132+
title VARCHAR(255) NOT NULL,
133+
secret_native BLOB NULL
134+
);
135+
```
136+
137+
The **passphrase** is the same string you configure for `MysqlAes` (`MYSQL_AES_KEY` or key file content). MySQL pads it to 16 bytes internally (AES-128-ECB, default `block_encryption_mode`).
138+
139+
### 2. INSERT in SQL (mysql client)
140+
141+
Replace the passphrase with your env value (never commit real keys):
142+
143+
```sql
144+
INSERT INTO my_secrets (title, secret_native)
145+
VALUES (
146+
'Invoice ACME',
147+
AES_ENCRYPT('password-doctrine-acme', 'change_me_mysql_aes_passphrase')
148+
);
149+
```
150+
151+
Verify (decrypt in the same session):
152+
153+
```sql
154+
SELECT id, title,
155+
CAST(AES_DECRYPT(secret_native, 'change_me_mysql_aes_passphrase') AS CHAR) AS secret_plain
156+
FROM my_secrets;
157+
```
158+
159+
Optional: inspect raw bytes as hex:
160+
161+
```sql
162+
SELECT id, title, HEX(secret_native) AS secret_hex FROM my_secrets;
163+
```
164+
165+
### 3. INSERT with bound parameters (Doctrine DBAL)
166+
167+
Recommended in PHP: bind **plaintext** and **passphrase**; MySQL performs encryption server-side.
168+
169+
```php
170+
use Doctrine\DBAL\Connection;
171+
172+
final class MySecretRepository
173+
{
174+
public function __construct(
175+
private readonly Connection $connection,
176+
private readonly string $mysqlAesKey, // inject %env(MYSQL_AES_KEY)%
177+
) {
178+
}
179+
180+
public function insertEncrypted(string $title, string $plaintext): int
181+
{
182+
$this->connection->executeStatement(
183+
'INSERT INTO my_secrets (title, secret_native)
184+
VALUES (:title, AES_ENCRYPT(:plain, :key))',
185+
[
186+
'title' => $title,
187+
'plain' => $plaintext,
188+
'key' => $this->mysqlAesKey,
189+
],
190+
);
191+
192+
return (int) $this->connection->lastInsertId();
193+
}
194+
}
195+
```
196+
197+
Symfony `services.yaml`:
198+
199+
```yaml
200+
services:
201+
App\Repository\MySecretRepository:
202+
arguments:
203+
$mysqlAesKey: '%env(MYSQL_AES_KEY)%'
204+
```
205+
206+
The demo implements the same pattern in `MysqlAesNoteRepository::insertWithAesEncrypt()` (symfony7/symfony8, route `/mysql-aes-note/sql/new`).
207+
208+
### 4. UPDATE an existing row
209+
210+
```sql
211+
UPDATE my_secrets
212+
SET secret_native = AES_ENCRYPT('new-secret-value', :passphrase)
213+
WHERE id = :id;
214+
```
215+
216+
```php
217+
$this->connection->executeStatement(
218+
'UPDATE my_secrets SET secret_native = AES_ENCRYPT(:plain, :key) WHERE id = :id',
219+
['plain' => $newSecret, 'key' => $this->mysqlAesKey, 'id' => $id],
220+
);
221+
```
222+
223+
### 5. INSERT with multiple columns (plain + encrypted)
224+
225+
Keep searchable metadata in **plain** columns; encrypt only sensitive fields:
226+
227+
```sql
228+
INSERT INTO my_secrets (title, secret_native)
229+
VALUES (
230+
:title,
231+
AES_ENCRYPT(:secret, :key)
232+
);
233+
-- title = plain (LIKE / INDEX possible)
234+
-- secret_native = binary ciphertext
235+
```
236+
237+
### 6. Two paths — do not mix formats on the same column
238+
239+
| Path | How to insert | Column example | Stored format |
240+
|------|----------------|----------------|---------------|
241+
| **Doctrine + `#[Encrypted('mysql_aes')]`** | `$em->persist($entity); $em->flush();` | `secret_orm` TEXT | MysqlAes ciphertext + `<ENC>` marker |
242+
| **Native MySQL** | `INSERT … AES_ENCRYPT(:plain, :key)` | `secret_native` BLOB | Raw MySQL AES blob, **no** `<ENC>` |
243+
| **SQL for ORM (advanced)** | `INSERT … CONCAT(AES_ENCRYPT(…), '<ENC>')` | `secret_orm` | MySQL AES blob + `<ENC>` (see section above) |
244+
245+
`AES_DECRYPT` on a Doctrine `secret_orm` value will **not** return the original text.
246+
`{{ x|decrypt('mysql_aes') }}` does **nothing** unless `x` ends with `<ENC>`.
247+
248+
### 7. Same key as the bundle `MysqlAes` encryptor
249+
250+
The passphrase in SQL must match the config used by PHP:
251+
252+
```yaml
253+
nowo_doctrine_encrypt:
254+
configs:
255+
mysql_aes:
256+
encryptor_class: MysqlAes
257+
secret_key_env_var: '%env(MYSQL_AES_KEY)%'
258+
```
259+
260+
Then rows inserted with `AES_ENCRYPT(..., :key)` and rows encrypted via `#[Encrypted('mysql_aes')]` use the **same algorithm**, but **different storage formats** unless you only use the native BLOB column for SQL inserts.
261+
262+
## Native SQL (repository / read)
34263

35264
For **pure** `AES_ENCRYPT` / `AES_DECRYPT` in SQL (no `<ENC>` marker), use a **BLOB** (or VARBINARY) column and bind the **passphrase** (not the derived key bytes):
36265

0 commit comments

Comments
 (0)