VillageSQL is a drop-in replacement for MySQL with extensions.
All examples in this guide work on VillageSQL. Install Now →
AES_ENCRYPT() built in. The problem is its default mode — aes-128-ecb — is insecure. ECB encryption is deterministic: the same plaintext encrypted with the same key always produces the same ciphertext. An attacker who sees your encrypted column can spot repeated values without ever knowing the key. VillageSQL’s encrypt() uses CBC mode with a random IV automatically, so identical inputs produce different output every time.
The MySQL Default: ECB Mode
MySQL’sAES_ENCRYPT(data, key) defaults to aes-128-ecb (block_encryption_mode). ECB — Electronic Codebook — encrypts each block independently with no randomness. Two rows with the same value produce the same ciphertext:
block_encryption_mode to use CBC, but then you have to manage the IV yourself: generate it, store it alongside the ciphertext, and pass it back to AES_DECRYPT() every time.
The Standard Workaround: Manual IV with AES-CBC
Using CBC mode correctly in MySQL requires storing and passing the IV explicitly:With VillageSQL: Automatic IV Management
VillageSQL’svsql_crypto extension adds encrypt() and decrypt(), which handle the IV automatically. The IV is generated fresh for each call and prepended to the ciphertext in the stored value. decrypt() extracts it without you having to think about it.
Key sizes
| Cipher type | Key length | When to use |
|---|---|---|
'aes' or 'aes-128' | 16 bytes | Lower-sensitivity data, performance-sensitive workloads |
'aes-192' | 24 bytes | Intermediate security |
'aes-256' | 32 bytes | PII, financial data, anything regulated — recommended default |
What to store
TheVARBINARY column holds the IV (16 bytes) prepended to the ciphertext. The encrypted size is roughly the plaintext size rounded up to the nearest AES block (16 bytes), plus 16 bytes for the IV. Size the column with headroom:
What This Doesn’t Cover
Column-level encryption in SQL protects data at rest from someone who has database access but not the key. It doesn’t protect against:- Application code that holds the key and queries decrypted data — an attacker who compromises the app gets both
- Queries that filter or sort on encrypted columns (you can’t
WHERE ssn = ?without decrypting)
Frequently Asked Questions
Where should I store the encryption key?
Not in the database. The key belongs in a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) or environment variable, loaded by the application at runtime. A key stored in the same database it encrypts provides no protection.Can I search on encrypted columns?
Not directly. Decrypting every row to find a match is a full table scan. Common patterns: store a deterministic hash of the value alongside the encrypted column for equality lookups (sha256(ssn) for SSN lookup), or decrypt in the application layer and build an encrypted search index.
What happens if I lose the key?
The data is unrecoverable. Back up keys separately from the database. Consider envelope encryption — wrapping the data key with a master key — so you can rotate the master key without re-encrypting everything.Can I encrypt a column with a DEFAULT expression?
Not currently. Non-deterministic functions (whichencrypt() is, due to the random IV) can’t be used in DEFAULT expressions. Generate the encrypted value in each INSERT.
Troubleshooting
| Problem | Solution |
|---|---|
FUNCTION encrypt does not exist | Run INSTALL EXTENSION vsql_crypto |
decrypt() returns NULL | Ciphertext is corrupted, key is wrong, or wrong cipher type specified |
| Encrypted values are identical for same plaintext | You’re using MySQL’s AES_ENCRYPT in ECB mode — switch to encrypt() |
| Column too small for ciphertext | Add 32 bytes to your VARBINARY size (16 for IV + up to 16 for padding) |
| Key is the wrong length | AES-128 needs 16 bytes, AES-192 needs 24, AES-256 needs 32 — check LENGTH(@key) |

