Brass wax seal stamp resting on a handwritten letter beside a finished red wax seal, illustrating how libsodium encryption protects WordPress form submissions on clinic sites.
|

Building forms with libsodium: encryption that doesn’t break the build

If your clinic site collects any patient information through a web form (intake, appointment requests, screening questionnaires, even a contact form that asks for date of birth), that data sits somewhere between the patient’s browser and your final destination. The question is what “sits” means. In transit, ideally over TLS. At rest, ideally encrypted. Both of those have been industry standard advice for a decade. Building libsodium WordPress forms is how I close the second gap on clinic projects.

The gap between the advice and most actual WordPress form plugins is wider than you would think. Libsodium WordPress forms close that gap with surprisingly little code. This post is about how I close that gap on forms I build for medical clients, using PHP’s built-in libsodium library. Not because libsodium is exotic. Because it is finally boring enough to use without footguns.

What most form plugins do

The popular WordPress form plugins (Gravity Forms, WPForms, Fluent Forms, Forminator) all store submissions in the WordPress database. Each submission is a row in a custom table, with the field values stored as serialized PHP, JSON, or individual columns depending on the plugin.

None of them encrypt the submission contents by default. The database is encrypted on disk if your host has full-disk encryption, but that protects against the disk being physically stolen, not against anyone with database access. An attacker who finds a WordPress vulnerability, gets read access to your wp_xxx_form_entries table, gets every submission in plaintext.

For most sites this is acceptable. For a medical site collecting PHI, it isn’t. The HIPAA Security Rule’s encryption requirements are addressable rather than mandatory, but “we didn’t encrypt the database” is a hard position to defend after a breach.

Why libsodium instead of openssl

PHP has shipped libsodium as part of the core since version 7.2 (2017). For PHP 8.x, which any current WordPress site should be running, libsodium is available out of the box, no extension to install, no Composer dependency.

The reason to use libsodium over PHP’s openssl extension is that libsodium’s API is designed to be hard to misuse. The encryption function picks the algorithm for you (XChaCha20-Poly1305 with authenticated encryption). The nonce generation is built in. The key sizes are fixed at 32 bytes. There is no “oh I forgot to set the IV” or “wait, ECB mode is broken?” failure mode. You call sodium_crypto_secretbox(), it works correctly, or it throws.

That matters for a small practice where the developer (often me, sometimes the client’s IT person) is not a cryptographer. The library should make the safe path the easy path. Libsodium does. OpenSSL doesn’t.

The libsodium WordPress forms encryption pattern

The pattern I use on form-handling plugins for medical clients looks roughly like this. The form posts to a custom endpoint. The handler validates the input, generates a fresh random nonce, and calls sodium_crypto_secretbox to encrypt each PHI field with a key stored outside the database.

$key = base64_decode(RSP_FORM_KEY);
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
$blob = base64_encode($nonce . $ciphertext);
// store $blob in the database

The nonce is stored alongside the ciphertext because you need it to decrypt. It does not need to be secret, only unique. Concatenating nonce and ciphertext into a single base64 blob keeps the storage column simple.

Decryption is the inverse:

If the ciphertext has been tampered with, sodium_crypto_secretbox_open returns false. That is the authenticated part of authenticated encryption. You don’t need a separate HMAC check.

Key management is the actual hard part

The encryption code is 10 lines. The key management is where it gets real. If the key is stored in wp-config.php, an attacker who reads wp-config.php has the key. If the key is stored in the database, an attacker with database access has both the key and the data. If the key is hardcoded in the plugin, an attacker who reads the plugin file has it.

For a small practice, the realistic answer is: store the key in wp-config.php, set file permissions to 600, and rely on the principle that an attacker who has read access to wp-config.php has already won regardless. That is not great. It is also the threshold above which you are talking about HSMs and key management services that small practices don’t have the budget or staff to maintain.

What you can do is rotate the key annually, keep an audit trail of when rotations happen, and make sure the key is excluded from backups (or, more practically, that backups are themselves encrypted with a separate key stored elsewhere).

What this protects against, and what it doesn’t

What libsodium encryption protects against: an attacker with read-only access to the database (a leaked backup, a SQL injection that exfiltrates data, a misconfigured replica), getting plaintext PHI. The ciphertext is useless without the key, which lives outside the database.

What it doesn’t protect against: an attacker who compromises the running PHP environment. If they can execute PHP, they can read wp-config.php, get the key, and decrypt anything. The encryption is a layer, not a fortress. It buys you time and limits blast radius. It does not make a compromised site safe.

This is the same logic I wrote about in what encrypted at rest actually means. Encryption is a control, not a state. It either reduces specific risks or it doesn’t. Pretending it does more than it does is how breach reports get worse.

Pairing this with auto-purge

Encryption protects data while it exists. The companion practice is making sure data only exists as long as it needs to. On the medical-form plugins I build, encrypted submissions are auto-purged after 14 days. By the time most attackers find their way in, the data they would have read has been overwritten.

I wrote about the retention math in auto-purge intervals. The short version: encryption plus a short retention window covers more of the real risk surface than encryption alone.

If you are building forms for a clinic right now

You probably don’t need to build a custom plugin. Most clinics can get away with a popular form plugin, configured to forward submissions to a HIPAA-aware system (a secure email service, a compliant CRM, or a patient portal) and to delete the WordPress copy within 24 hours. The form plugin holds the data briefly. The compliant destination holds it long-term.

The case for libsodium-encrypted custom forms is narrower: when you genuinely need to store PHI on your own WordPress server, for a reason that justifies the engineering cost. Screening questionnaires that need to render results on-screen for the clinician. Intake forms where the staff prefers to read the answers in the WordPress admin. Specific workflows where the third-party tool doesn’t fit.

For those cases, libsodium gives you a path that doesn’t break the build, doesn’t require new dependencies, and doesn’t put you in the awkward position of explaining to an auditor why you rolled your own crypto. You didn’t. You used the one that ships with the language.

Similar Posts