Type something to search...
How to encrypt sensitive data on a website?

How to encrypt sensitive data on a website?

You encrypt sensitive data on a website in three places: in transit, by serving everything over HTTPS with modern TLS; at rest, by encrypting the disks, databases, and backups where data is stored; and at the field level, by encrypting especially sensitive values like national ID numbers or API tokens with a well-tested library such as libsodium before saving them. Passwords are the exception: they should be hashed with a slow algorithm like Argon2id or bcrypt, never encrypted. And none of it works without good key management, which means keeping encryption keys separate from the data they protect.

Encryption sounds intimidating, but most of the hard work is already done for you by modern tools. The challenge is knowing which kind of protection applies to which data, and avoiding the common mistakes that make encryption useless. This article explains the different layers of encryption, shows working code for field-level encryption in PHP, WordPress, and Node.js, and covers how to manage keys safely.

What Counts as Sensitive Data?

Before encrypting anything, identify what you actually hold. Sensitive data on a typical website includes:

  • Authentication data: Passwords, session tokens, password reset tokens, two-factor secrets.
  • Personal data: Names, addresses, phone numbers, dates of birth, and especially government IDs.
  • Financial data: Bank details, payment information, invoices.
  • Health or special category data: Medical information, which carries stricter legal requirements in many regions.
  • Business secrets: API keys, third-party credentials, private documents.
  • Form submissions: Contact forms and applications often contain far more personal data than expected.

The best way to protect sensitive data is not to store it at all. If you only need a card for one payment, let a payment provider like Stripe or PayPal handle it, so card numbers never touch your server. Delete form submissions you no longer need. Every field you don't store is a field that can't leak.

Encryption vs Hashing

These two terms are often confused, and using the wrong one is a common mistake.

  • Encryption is reversible. With the right key, you can decrypt the data back to its original form. Use it for data you need to read later, like a customer's address or an API token.
  • Hashing is one-way. You can't turn a hash back into the original value. Use it for data you only need to check, like passwords.

Encoding methods like Base64 are neither. They're trivially reversible and provide no security at all.

Layer 1: Encrypt Data in Transit With HTTPS

Every page on your website should be served over HTTPS. TLS encrypts the connection between the visitor's browser and your server, protecting logins, form submissions, and cookies from being read or altered along the way.

  1. Get a certificate: Let's Encrypt provides free certificates, and most hosts install them automatically.
  2. Redirect all HTTP traffic to HTTPS.
  3. Use modern TLS versions: Enable TLS 1.2 and 1.3, and disable older versions.
  4. Enable HSTS: Once you're sure HTTPS works everywhere, the Strict-Transport-Security header tells browsers never to use HTTP for your site.

A sensible Nginx TLS configuration looks like this:

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

The http2 on; directive applies to Nginx 1.25.1 and newer; older versions use listen 443 ssl http2; instead. Mozilla's SSL Configuration Generator is a good reference for up-to-date cipher settings. If you're moving an existing WordPress site to HTTPS, we have a separate guide covering that process.

Don't forget connections behind the scenes. If your application talks to a database or API on another server, those connections should be encrypted too.

Layer 2: Encrypt Data at Rest

Encryption at rest protects data if someone gets hold of the physical storage, such as a stolen disk, a leaked snapshot, or a misconfigured backup bucket.

  • Disk encryption: Most cloud providers, including AWS, Google Cloud, Azure, and DigitalOcean, encrypt block storage by default or offer it as an option. On your own servers, Linux full-disk encryption with LUKS is common.
  • Database encryption: MySQL and MariaDB support InnoDB tablespace encryption, and managed databases like Amazon RDS or Google Cloud SQL offer encryption at rest.
  • Backup encryption: Backups contain your entire site and database. Make sure they're encrypted, whether by your backup tool or your storage provider.

It's important to understand what at-rest encryption doesn't protect against. If an attacker compromises your application or gets your database credentials, the database will happily decrypt data for them, because they're talking to it as a legitimate user. At-rest encryption protects the storage, not the application. That's where field-level encryption comes in.

Layer 3: Field-Level Encryption

Field-level (or application-level) encryption means your application encrypts specific values before saving them, and decrypts them only when needed. Even if an attacker dumps your database through SQL injection or a stolen backup, those fields remain unreadable without the key, which is stored elsewhere.

Use it for your most sensitive fields: government ID numbers, bank details, API tokens for third-party services, or private notes.

The Golden Rule: Don't Build Your Own Crypto

Never invent your own encryption scheme or use low-level primitives directly unless you really know what you're doing. Use a high-level, authenticated encryption library that handles nonces, padding, and integrity checks for you. Good options:

  • PHP: libsodium (built into PHP 7.2 and later via the sodium_* functions).
  • Node.js: The built-in crypto module with AES-256-GCM, or libsodium bindings.
  • Python: The cryptography package, particularly Fernet or AES-GCM.

Field-Level Encryption in PHP With libsodium

First, generate a key once and store it securely, outside your web root and outside version control:

php -r "echo base64_encode(random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES)), PHP_EOL;"

Then use it to encrypt and decrypt values:

<?php
function sajjad_encryption_key(): string {
    $key = base64_decode( getenv( 'APP_ENCRYPTION_KEY' ) ?: '', true );
    if ( false === $key || SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== strlen( $key ) ) {
        throw new RuntimeException( 'Encryption key is missing or invalid.' );
    }
    return $key;
}

function sajjad_encrypt( string $plaintext ): string {
    $nonce      = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
    $ciphertext = sodium_crypto_secretbox( $plaintext, $nonce, sajjad_encryption_key() );
    return base64_encode( $nonce . $ciphertext );
}

function sajjad_decrypt( string $encoded ): ?string {
    $decoded = base64_decode( $encoded, true );
    if ( false === $decoded || strlen( $decoded ) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ) {
        return null;
    }

    $nonce      = substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
    $ciphertext = substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
    $plaintext  = sodium_crypto_secretbox_open( $ciphertext, $nonce, sajjad_encryption_key() );

    return false === $plaintext ? null : $plaintext;
}

sodium_crypto_secretbox uses XSalsa20-Poly1305, which is authenticated encryption: if anyone tampers with the stored value, decryption fails instead of returning corrupted data. A fresh random nonce is generated for every encryption and stored alongside the ciphertext, which is safe and expected.

Using It in WordPress

In WordPress, define the key in wp-config.php (above the "That's all, stop editing!" line) or, better, set it as an environment variable through your host:

// wp-config.php
define( 'SAJJAD_ENCRYPTION_KEY', getenv( 'APP_ENCRYPTION_KEY' ) );

You could then adapt sajjad_encryption_key() to read the constant, and use the helpers when saving and reading user meta in a custom plugin:

<?php
function sajjad_save_tax_id( int $user_id, string $tax_id ): void {
    $tax_id = sanitize_text_field( $tax_id );
    update_user_meta( $user_id, 'sajjad_tax_id', sajjad_encrypt( $tax_id ) );
}

function sajjad_get_tax_id( int $user_id ): ?string {
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        return null;
    }
    $stored = get_user_meta( $user_id, 'sajjad_tax_id', true );
    return $stored ? sajjad_decrypt( $stored ) : null;
}

Notice the capability check before decrypting. Encryption protects data in the database, but your code still decides who can see the decrypted value.

Field-Level Encryption in Node.js

With Node's built-in crypto module and AES-256-GCM:

import crypto from "node:crypto";

const key = Buffer.from(process.env.APP_ENCRYPTION_KEY, "base64"); // 32 bytes

export function encrypt(plaintext) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  const ciphertext = Buffer.concat([
    cipher.update(plaintext, "utf8"),
    cipher.final(),
  ]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([iv, tag, ciphertext]).toString("base64");
}

export function decrypt(encoded) {
  const data = Buffer.from(encoded, "base64");
  const iv = data.subarray(0, 12);
  const tag = data.subarray(12, 28);
  const ciphertext = data.subarray(28);
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([
    decipher.update(ciphertext),
    decipher.final(),
  ]).toString("utf8");
}

Generate the key with openssl rand -base64 32. Never reuse an IV with the same key in GCM mode, which is why a random 12-byte IV is generated for every call.

Searching Encrypted Fields

Encrypted values can't be searched with normal SQL queries, because the same input produces different ciphertext each time. If you need to look records up by an encrypted value, such as finding a user by their tax ID, store a separate keyed hash (a "blind index") alongside it:

<?php
function sajjad_blind_index( string $value ): string {
    $index_key = base64_decode( getenv( 'APP_INDEX_KEY' ), true );
    return hash_hmac( 'sha256', strtolower( trim( $value ) ), $index_key );
}

Use a different key from your encryption key, and only add blind indexes for fields you genuinely need to search.

Hashing Passwords Correctly

Passwords should never be encrypted or stored in plain text. They should be hashed with an algorithm designed to be slow, so that cracking leaked hashes is expensive.

In plain PHP, use the built-in password API:

<?php
// When the user sets a password
$hash = password_hash( $password, PASSWORD_ARGON2ID );

// When the user logs in
if ( password_verify( $password, $stored_hash ) ) {
    if ( password_needs_rehash( $stored_hash, PASSWORD_ARGON2ID ) ) {
        $new_hash = password_hash( $password, PASSWORD_ARGON2ID );
        // Save $new_hash to the database.
    }
    // Log the user in.
}

PASSWORD_ARGON2ID requires PHP to be compiled with Argon2 support, which is common but not universal. PASSWORD_DEFAULT (currently bcrypt) is a safe fallback.

In WordPress, never handle password hashing yourself. Use wp_set_password(), wp_hash_password(), and wp_check_password(). Since WordPress 6.8, core uses bcrypt for new password hashes by default. Never use fast hashes like MD5 or SHA-256 on their own for passwords.

The same idea applies to other secrets you only need to verify, like password reset tokens or API keys you issue to users: store a hash, not the original.

Key Management: The Hard Part

Encryption is only as strong as the protection of its keys. If your encryption key sits in the same database as the encrypted data, an attacker who steals the database gets both.

Good key management practices:

  1. Keep keys separate from data: Store keys in environment variables, a secrets manager, or a key management service, never in the database or in code committed to Git.
  2. Use a managed key service when possible: AWS KMS, Google Cloud KMS, Azure Key Vault, and HashiCorp Vault can store master keys and perform encryption operations without exposing the raw key.
  3. Limit who can read keys: Only the application and a small number of administrators should have access.
  4. Plan for rotation: Include a key version identifier with each encrypted value, so you can introduce a new key and re-encrypt data gradually.
  5. Back up keys securely: If you lose the key, the encrypted data is gone forever. Keep an encrypted, offline copy in a secure location.
  6. Use envelope encryption for larger systems: Encrypt data with a data key, and encrypt the data key with a master key held in a KMS.

Common Encryption Mistakes

  • Storing the key next to the data: Defeats the purpose entirely.
  • Using ECB mode or unauthenticated encryption: Leaks patterns or allows tampering. Use authenticated modes like GCM or libsodium's secretbox.
  • Reusing nonces or IVs: Can break the security of the encryption.
  • Encrypting passwords instead of hashing them.
  • Logging sensitive data: Decrypted values end up in error logs, debug output, or analytics tools.
  • Forgetting backups and exports: CSV exports and database dumps often sit unencrypted in downloads folders or email inboxes.
  • Using deprecated tools: The old PHP mcrypt extension was removed long ago and shouldn't be used.

FAQ: Encrypting Sensitive Website Data

No. HTTPS protects data while it travels between the browser and your server. Once it's stored, you also need encryption at rest and, for the most sensitive fields, application-level encryption.

No. Passwords should be hashed with a slow, salted algorithm like Argon2id or bcrypt so they can't be reversed. Encryption is reversible, which is exactly what you don't want for passwords.

Store it outside the database and outside version control, ideally in an environment variable, a secrets manager, or a key management service like AWS KMS or HashiCorp Vault.

WordPress hashes user passwords, but it doesn't encrypt other data like user meta or form entries by default. If you store sensitive fields, you need to encrypt them in your own code or use a plugin designed for that purpose.

Use a high-level authenticated encryption library rather than choosing primitives yourself. libsodium's secretbox (XSalsa20-Poly1305) and AES-256-GCM are both strong, widely used choices.

The encrypted data becomes permanently unreadable. Keep a secure, offline backup of your keys and document how to restore them.


Conclusion

Encrypting sensitive data on a website is about matching the right protection to the right data. HTTPS protects everything in transit, at-rest encryption protects your disks and backups, field-level encryption protects your most sensitive values even if the database is dumped, and slow password hashing protects credentials. Each layer covers a different threat, and together they make a data breach far less damaging.

Start by reducing what you store, then make sure HTTPS and at-rest encryption are in place. For the fields that truly matter, use a proven library like libsodium, keep your keys well away from your data, and plan for rotation and backup from the beginning. With those foundations, you can handle sensitive information responsibly without needing to become a cryptographer.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
How does GDPR affect website security?

How does GDPR affect website security?

GDPR affects website security by turning it from a good habit into a legal obligation. If your website collects personal data from people in the EU (

Dive Deeper