DEV Community

Ilya Mikhasik
Ilya Mikhasik

Posted on Edited on

Our System Series: Sensitive Data Encryption

Previous: Registries

The registry layer gives us a consistent way to store and retrieve users, profiles, links, and other entities. The next question is how to protect sensitive values before those entities reach the database.

The answer in this system is to encrypt selected fields in the User Profiles Service. As shown in the signup workflow, this service sits between the Signup Service and User Profiles Registries. It prepares user and profile data before it is persisted, including encryption of protected values.

The registry receives encrypted values and stores them through its normal interface. It does not need to understand which fields are sensitive or implement encryption rules itself.

Encrypting at the registry boundary

The encryption module is implemented as decorators around the shared registry-interaction function:
@encrypt_payload
@decrypt_payload
async def interact_with_registry(...):
...

Before a POST, PUT, or PATCH request is sent, the encryption decorator examines the target registry and encrypts the relevant fields. After a GET request completes, the decryption decorator reverses that transformation before the result is returned to the User Profiles Service.

The registry name determines which rules apply.

The same distinction is applied when decrypting individual user and profile records, as well as collection responses from users and profiles registries.

This approach keeps encryption close to the service that understands the meaning of the data. It also prevents encryption and decryption calls from being copied into every method that interacts with user or profile records.

What is encrypted

The system uses field-level encryption rather than encrypting the complete record. Some profile data is nested. For nested profile data, the module encrypts the values inside media, email, and phone entries while preserving the surrounding JSON structure.

The record’s overall structure remains available. A profile can still be identified, associated with a project, checked for status, and connected to other entities. What is protected are the values that reveal personal information.

The encryption format

The module uses AESSIV from the Python cryptography package and creates a 512-bit encryption key.

Before encryption, a value is serialized as JSON. This allows the same function to work with strings and structured values. The ciphertext is then encoded with URL-safe Base64 before it is stored in a registry record. That makes the encrypted result safe to place inside JSON payloads and send through the registry API.

AES-SIV is an authenticated-encryption construction. In this implementation, no nonce is supplied, so the encryption is deterministic: encrypting the same normalized value with the same key produces the same ciphertext. The cryptography documentation describes this usage as deterministic authenticated encryption.

That property is useful because it allows the system to search protected values by encrypting the search term first. The trade-off is that identical plaintext values produce identical ciphertext values, revealing equality between stored values.

Tracking key versions

When the module encrypts at least one field, it records the active key version in the object metadata.

When a record is returned from the registry, the module checks this value before attempting decryption.

  • A record with the current key version is decrypted.
  • A record without a key_version is returned unchanged.
  • A record marked with another key version is not decrypted with the current key.

This supports a transition in which legacy plaintext records and newly encrypted records can coexist. It also gives every encrypted record the information needed for a future key-rotation process.

At present, the module decrypts records only when their version matches the key configured in the service. A full rotation design would later need to support reading older versions, re-encrypting records in batches, and retiring older keys after the migration is complete.

Searching protected profile data

Encryption changes how the system searches user and profile records.

A normal database query cannot compare a plaintext value such as Ivan with an encrypted value stored in the profile record. To handle this, profile search is performed twice.

First, the service searches legacy records using plaintext filters. Then it normalizes and encrypts the same values and performs a second search for encrypted records.

For example, a name is normalized before encryption and the resulting ciphertext is then used in an exact registry lookup.

Phone numbers are normalized and validated before encryption. Email addresses are converted to lowercase first. This is necessary because deterministic encryption only produces matching ciphertext when the input is identical.

The two result sets are merged by profile_id, so the same profile is not returned twice.

This allows plaintext and encrypted records to coexist while encryption is introduced gradually rather than requiring every historical record to be migrated at once.

What the design preserves

The purpose of the module is not only to encrypt data. It must also preserve the behavior needed by the application.

The design allows the system to:

  • Encrypt selected personal and authentication-related fields before storage
  • Keep encryption rules in one shared module
  • Continue using the same registry APIs
  • Read legacy unencrypted records during the transition
  • Find encrypted records through normalized exact-match searches
  • Identify which key version was used for a record

There is an important limitation: deterministic encryption allows equality matching, but not partial matching. Searches such as “starts with,” “contains,” or fuzzy text matching cannot operate directly on the encrypted values. The encrypted search path therefore uses exact comparisons.

Closing perspective

The User Profiles Service applies encryption at the point where sensitive user and profile data enters or leaves the registry layer. This keeps protected values out of plaintext database storage while allowing the registry architecture to remain standardized and reusable.

The next step for this design is a formal key-rotation workflow: support multiple key versions for decryption, re-encrypt records in controlled batches, verify that the migration succeeded, and retire the old key only after the process is complete. These tasks are in the development pipeline.

In the next article, I will describe the Event Processor. During user signup, the Signup Service sends an event after user and profile creation is complete. The Event Processor receives that event and handles follow-up work, including sending the signup email to the new user.

Next: Event Processing

Top comments (0)