VillageSQL is a drop-in replacement for MySQL with extensions.
All examples in this guide work on VillageSQL. Install Now →
AES_ENCRYPT() built in, but it defaults to an insecure mode. This guide covers the trade-offs, how to use AES correctly in MySQL, and when to do encryption in SQL versus the application layer.
How AES Encryption Works in MySQL
MySQL’sAES_ENCRYPT(data, key) uses 128-bit AES with the key derived from the key string. The default mode is ECB — Electronic Codebook — where each 16-byte block is encrypted independently. ECB is deterministic: the same 16-byte input always produces the same 16-byte output. That makes patterns visible in the ciphertext.
block_encryption_mode and managing the IV yourself:
With VillageSQL: Automatic CBC and IV Handling
VillageSQL’svsql_crypto extension provides encrypt() and decrypt(), which use AES-CBC with a random IV generated per call. The IV is prepended to the ciphertext — no extra column, no extra parameter.
AES key sizes
| Cipher | Key length | Security level |
|---|---|---|
'aes' or 'aes-128' | 16 bytes | 128-bit — adequate for most use cases |
'aes-192' | 24 bytes | 192-bit |
'aes-256' | 32 bytes | 256-bit — use for regulated data (PII, HIPAA, PCI) |
LENGTH(@key) if you’re getting unexpected behavior.
Storing encrypted data
encrypt() returns VARBINARY. The stored value includes a 16-byte IV prepended to the ciphertext. Size your column accordingly:
MySQL vs VillageSQL Comparison
AES_ENCRYPT() (MySQL default) | encrypt() (VillageSQL) | |
|---|---|---|
| Default mode | ECB (insecure) | CBC with random IV |
| IV management | Manual — store separately | Automatic — prepended to output |
| Schema impact | Needs IV column for CBC | One column for ciphertext + IV |
| Repeated plaintext | Same ciphertext (ECB) | Different ciphertext each time |
| Key size control | Via block_encryption_mode setting | Via cipher type parameter |
When to Encrypt in SQL vs. Application Layer
Encrypting in SQL is convenient but has trade-offs:| SQL-layer encryption | Application-layer encryption | |
|---|---|---|
| Key location | In queries/triggers (risky if visible in logs) | In secrets manager, not in SQL |
| Searchability | Can’t filter on encrypted values | Same limitation |
| Bulk operations | Encryption happens in DB | Must go through app code |
| Key rotation | Requires re-encrypting all data in SQL | Can decrypt with old key, re-encrypt with new key in app |
| Audit trail | Query logs may expose plaintext | App controls what’s logged |
Frequently Asked Questions
What’s the difference between symmetric and asymmetric encryption?
Symmetric encryption (AES) uses the same key to encrypt and decrypt. It’s fast and well-suited for bulk data. Asymmetric encryption (RSA) uses a public key to encrypt and a private key to decrypt — useful for key exchange, but too slow for bulk data. For storing sensitive column data, symmetric AES is the right choice.Can I search on an encrypted column?
Not directly. To find rows by an encrypted value, you’d have to decrypt every row — a full table scan. A common workaround: store a keyed hash (HMAC) of the plaintext in a separate indexed column for equality lookups. This leaks that two rows have the same value but not what the value is.How do I rotate encryption keys?
Read every row, decrypt with the old key, re-encrypt with the new key, write back. For large tables, do this in batches. This is the main operational cost of SQL-layer encryption. Envelope encryption (encrypting the data key with a master key) lets you rotate the master key without touching the data.Does AES encryption protect against SQL injection?
No. Encryption protects the stored values. SQL injection is an input validation problem — an attacker who can run arbitrary SQL can calldecrypt() directly.
Troubleshooting
| Problem | Solution |
|---|---|
FUNCTION encrypt does not exist | Run INSTALL EXTENSION vsql_crypto |
decrypt() returns NULL | Wrong key, wrong cipher type, or corrupted ciphertext |
AES_ENCRYPT produces same output for same inputs | You’re using ECB mode — switch to encrypt() or set block_encryption_mode = 'aes-256-cbc' with manual IV |
| Column too small | Add 32 bytes to VARBINARY size (16 for IV + up to 16 for AES block padding) |
| Key length wrong | Check LENGTH(@key) — should be 16, 24, or 32 bytes exactly |

