Back to Blog
ArchitectureJune 24, 20264 min read

API Security: Implementing Field-Level Encryption via Envelope Encryption

Master API security through field-level encryption and envelope encryption. Protect sensitive data in transit and at rest without sacrificing system performance.

API securityencryptionsystem designdata privacycryptographyAPIArchitectureBackend

We once spent three weeks debugging a data leak that originated not from a compromised database, but from a logging middleware that was too "helpful." It had captured unmasked PII from a request body, storing it in plain text across our distributed logging cluster. That incident forced us to stop relying solely on TLS for data privacy and start treating individual fields as untrusted entities, regardless of where they sat in our stack.

If you’re handling sensitive information, you need a strategy for field-level encryption that doesn't turn your application code into a cryptographic graveyard.

The Problem with Blanket Encryption

Many teams start by encrypting the entire request payload or the entire database column. Both approaches fall short. Full-payload encryption breaks load balancers and WAFs that need to inspect traffic, while full-column encryption makes querying or filtering data impossible without decrypting everything in memory first.

The solution is envelope encryption. By using a Data Encryption Key (DEK) to encrypt the field and a Key Encryption Key (KEK) to wrap that DEK, you gain the ability to rotate keys and control access at a granular level.

Architecting Envelope Encryption

In our current stack, we use a dedicated Key Management Service (KMS) like AWS KMS or HashiCorp Vault. The workflow follows this cycle:

  1. Request Phase: The application requests a plaintext DEK and a corresponding encrypted DEK from the KMS.
  2. Encryption: The application encrypts the sensitive field (e.g., credit_card_number) using the plaintext DEK.
  3. Transmission: The field is replaced with a JSON object containing the encrypted DEK and the ciphertext.
  4. Storage: The data is written to the database in this "envelope" format.

To keep this clean, I highly recommend looking at how you handle your API security: decoupling field-level authorization from controllers. By moving the encryption logic into a decorator or an interceptor, you ensure that the controller logic remains agnostic to the underlying security implementation.

Implementation Details

When implementing this, performance is usually the first concern. Encrypting every field adds latency, usually around 10-15ms per request due to network round-trips to the KMS. To mitigate this, we implemented a local DEK cache with a TTL of roughly 5 minutes.

Here is a simplified view of the structure we send over the wire:

JSON
{
  "user_id": "12345",
  "email": "user@example.com",
  "pii_data": {
    "ciphertext": "aBcDeFgHiJkLmNoPqRsTuVwXyZ",
    "encrypted_dek": "kms_wrapped_key_blob",
    "algorithm": "aes-256-gcm"
  }
}

By keeping the encrypted_dek attached to the field, the service that reads the data doesn't need to know the origin of the key; it simply sends the blob back to the KMS to unwrap the DEK. This is crucial for distributed systems architecture, where different services might own different parts of the data lifecycle.

Handling Trade-offs

You’ll inevitably run into the "searchability" problem. Once you encrypt a field, you can't run WHERE email = '...' queries. We solved this by creating a deterministic "blind index"—a one-way HMAC of the field—stored in a separate column.

Before you go down this path, consider these constraints:

  • Key Rotation: If you rotate your KEK, you must re-wrap all existing DEKs. This is a background job nightmare if not planned early.
  • Auditability: You need to track who accessed which key. Integrating this with your API architecture audit logs: implementing immutable event sourcing is non-negotiable for compliance.
  • Complexity: If your team isn't familiar with crypto primitives, keep the implementation behind a thin internal library. Don't let developers roll their own Encrypt() functions.

What I’d Do Differently

Looking back at our first rollout, I would have invested more in a schema-registry approach. We had different services using different padding schemes for AES, which led to a weekend of "decryption failed" errors during a migration.

If I were starting over, I'd define the encryption metadata (algorithm, version, key-id) as a mandatory header or a schema property enforced by the gateway. Also, don't forget to propagate your trace context during the KMS call; if the KMS call fails, you want to know exactly which request triggered the latency spike.

Field-level encryption is a heavy lift, but it’s the only way to achieve true data privacy in a modern, distributed environment. It turns your database from a "bag of secrets" into a reliable, encrypted vault. Just remember: the goal is to make the security invisible to the business logic, not to make the architecture impossible to maintain.

Similar Posts