Quantum-Safe Cloud -- Parte 3

QuantumAPI EaaS: Cifra lo que quieras

#security #post-quantum #encryption #quantumapi #dotnet #postgresql

El problema

Secretos en QuantumVault. Connection strings fuera de los archivos de configuración. Buen comienzo.

Pero los secretos son un problema. Los datos sensibles son otro.

Cuando un usuario crea una cuenta en tu aplicación, su email, número de teléfono y fecha de nacimiento van a tu base de datos PostgreSQL. La conexión a la base de datos está cifrada (TLS). La propia base de datos puede tener cifrado de disco. Pero los datos dentro de las tablas se guardan como texto plano. Si un atacante consigue acceso de lectura a tu base de datos — un backup filtrado, un permiso mal configurado, una cuenta de admin comprometida — se lleva los datos de todos.

La defensa estándar es el cifrado a nivel de aplicación: cifrar los campos sensibles antes de escribirlos en la base de datos, descifrar después de leerlos. Los datos en la base de datos son ciphertext. Un archivo de backup es inútil sin la clave.

El problema con el cifrado a nivel de aplicación es la gestión de claves. Necesitas una clave de cifrado. ¿Dónde la guardas? ¿Cómo la rotas? Si la cifras con RSA, vuelves al mismo problema de harvest-now-decrypt-later del artículo 1. Si la guardas en un archivo, has cambiado un problema de datos en texto plano por un problema de clave en texto plano.

La solución elegante: encryption as a service. Envías el texto plano a una API, recibes ciphertext. Envías el ciphertext de vuelta a la API, recibes el texto plano. La clave nunca sale del servicio. Tú nunca la gestionas. Y si el servicio usa ML-KEM por debajo, el cifrado sobrevive a los ordenadores cuánticos.

Eso es lo que hace QuantumAPI EaaS.

La solución

El cifrado híbrido combina dos algoritmos:

  1. ML-KEM-768 (key encapsulation mechanism) — genera un secreto compartido. Esta es la parte post-quantum.
  2. AES-256-GCM (cifrado simétrico) — cifra los datos reales usando ese secreto compartido. Esta es la parte rápida.

¿Por qué ambos? ML-KEM trabaja con inputs de tamaño fijo. No está diseñado para cifrar datos arbitrarios directamente. AES-256-GCM es rápido y está diseñado para datos en volumen, pero su clave necesita venir de algún sitio seguro. Combinarlos te da lo mejor de ambos: seguridad post-quantum para el intercambio de claves, y rendimiento para los datos.

Por debajo, cuando llamas a POST /api/v1/encrypt:

  1. quantumAPI genera un secreto compartido nuevo usando ML-KEM-768 y la clave pública del tenant
  2. Deriva una clave AES de 32 bytes de ese secreto compartido usando HKDF-SHA256
  3. Cifra tus datos con AES-256-GCM usando esa clave
  4. Devuelve un único blob opaco: el ciphertext de ML-KEM + el nonce de AES + el ciphertext de AES + el authentication tag

Guardas el blob. Lo envías de vuelta para descifrar. El material de clave se computa nuevo cada vez desde la clave privada de ML-KEM — nunca se almacena junto a los datos.

El resultado: tu base de datos tiene ciphertext que necesita la clave privada de ML-KEM para descifrarse. Esa clave vive en QuantumVault, generada con QRNG, y nunca sale de quantumAPI.

Execute

Vamos a añadir cifrado a nivel de campo a la API de usuarios de la serie ATLAS+GOTCHA. Email y número de teléfono se cifrarán antes de almacenarse y se descifrarán al recuperarlos.

ATLAS para esta tarea

[A] ARCHITECT
  Add field-level encryption to the User entity.
  Encrypted fields: Email, PhoneNumber.
  SearchableEmail: SHA3-256 hash of lowercase email (for lookups).
  Out of scope: full-text search on encrypted fields (a separate problem).

[T] TRACE
  Create user → encrypt Email + PhoneNumber via EaaS → store ciphertext
  Get user → retrieve ciphertext → decrypt via EaaS → return plaintext to caller
  Login (lookup by email) → hash incoming email → compare against SearchableEmail

[L] LINK
  App → QuantumAPI EaaS: HTTPS, X-Api-Key
  EaaS → QuantumVault (internal): key resolution
  App → PostgreSQL: Npgsql, connection string from QuantumVault

[A] ASSEMBLE
  1. Add IEncryptionService abstraction
  2. Implement QuantumApiEncryptionService using the SDK
  3. Update User entity: Email → EmailCiphertext, add SearchableEmail
  4. Update IUserRepository create/read methods
  5. Migration: encrypt existing rows

[S] STRESS-TEST
  What if EaaS is unreachable during a write?
    → Don't write the row. Fail fast, let the caller retry.
  What if EaaS is unreachable during a read?
    → Return error. Don't return ciphertext to the caller.
  What about performance?
    → Batch encrypt: encrypt multiple fields in parallel, not sequentially.

GOTCHA prompt

=== GOALS ===
Add field-level encryption to the User entity in the users-api (.NET 10).
Encrypt Email and PhoneNumber using QuantumAPI EaaS before writing to PostgreSQL.
Store a SHA3-256 hash of the lowercase email for login lookups (SearchableEmail).
Decrypt on read before returning to callers.

=== ORCHESTRATION ===
1. IEncryptionService interface: EncryptAsync(string plaintext) → string ciphertext
                                 DecryptAsync(string ciphertext) → string plaintext
2. QuantumApiEncryptionService: implements IEncryptionService using QuantumApiClient
3. Update User entity: EmailCiphertext (string), PhoneNumberCiphertext (string?),
   SearchableEmail (string, indexed), remove plain Email + PhoneNumber columns
4. Update UserRepository.CreateAsync: encrypt before insert
5. Update UserRepository.GetByIdAsync: decrypt after select
6. Update UserRepository.GetByEmailAsync: hash input, query SearchableEmail

=== TOOLS ===
- QuantumAPI.Client NuGet package
- QuantumApiClient.Encryption.EncryptAsync / DecryptAsync
- System.Security.Cryptography.SHA3_256 (.NET 8+)
- Entity Framework Core + Npgsql (existing)

=== CONTEXT ===
- Existing User entity has Email (string), PhoneNumber (string?), PasswordHash (string)
- PasswordHash is already safe — Argon2id, no need to encrypt
- Repository pattern: IUserRepository / UserRepository
- EF Core migrations for the column rename

=== HEURISTICS ===
DO:
- Encrypt in the repository layer, not in the service or controller
- Run EncryptAsync calls in parallel (Task.WhenAll) when encrypting multiple fields
- Use lowercase + trim before hashing email for SearchableEmail
- Inject IEncryptionService — don't instantiate QuantumApiClient directly in the repository

DON'T:
- Don't cache decrypted values in memory longer than the request lifetime
- Don't log decrypted field values
- Don't put the QuantumAPI key in appsettings.json (environment variable only)

=== ARGS ===
Algorithm: ML-KEM-768 (default, no need to specify — use the tenant default key)
SearchableEmail column: indexed, unique
Environment variable: QUANTUMAPI__APIKEY

La implementación

// IEncryptionService.cs
public interface IEncryptionService
{
    Task<string> EncryptAsync(string plaintext);
    Task<string> DecryptAsync(string ciphertext);
}
// QuantumApiEncryptionService.cs
public class QuantumApiEncryptionService : IEncryptionService
{
    private readonly QuantumApiClient _client;

    public QuantumApiEncryptionService(QuantumApiClient client)
    {
        _client = client;
    }

    public async Task<string> EncryptAsync(string plaintext)
    {
        var result = await _client.Encryption.EncryptAsync(new EncryptRequest
        {
            Plaintext = plaintext,
            Encoding = "utf8"
        });
        return result.EncryptedPayload;
    }

    public async Task<string> DecryptAsync(string ciphertext)
    {
        var result = await _client.Encryption.DecryptAsync(new DecryptRequest
        {
            EncryptedPayload = ciphertext
        });
        return result.Plaintext;
    }
}
// User.cs (updated entity)
public class User
{
    public Guid Id { get; set; }
    public string EmailCiphertext { get; set; } = string.Empty;
    public string SearchableEmail { get; set; } = string.Empty; // SHA3-256 hash
    public string? PhoneNumberCiphertext { get; set; }
    public string PasswordHash { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public DateTimeOffset CreatedAt { get; set; }
    public bool IsDeleted { get; set; }
}
// UserRepository.cs (relevant methods)
public class UserRepository : IUserRepository
{
    private readonly UsersDbContext _db;
    private readonly IEncryptionService _encryption;

    public UserRepository(UsersDbContext db, IEncryptionService encryption)
    {
        _db = db;
        _encryption = encryption;
    }

    public async Task<User> CreateAsync(CreateUserDto dto)
    {
        // Encrypt in parallel — don't await sequentially
        var (emailCiphertext, phoneCiphertext) = await EncryptUserFieldsAsync(
            dto.Email, dto.PhoneNumber);

        var user = new User
        {
            Id = Guid.NewGuid(),
            EmailCiphertext = emailCiphertext,
            SearchableEmail = HashEmail(dto.Email),
            PhoneNumberCiphertext = phoneCiphertext,
            PasswordHash = Argon2id.Hash(dto.Password),
            FirstName = dto.FirstName,
            LastName = dto.LastName,
            CreatedAt = DateTimeOffset.UtcNow
        };

        _db.Users.Add(user);
        await _db.SaveChangesAsync();
        return user;
    }

    public async Task<UserDto?> GetByIdAsync(Guid id)
    {
        var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
        if (user is null) return null;

        return await DecryptUserAsync(user);
    }

    public async Task<UserDto?> GetByEmailAsync(string email)
    {
        var searchable = HashEmail(email);
        var user = await _db.Users.FirstOrDefaultAsync(u => u.SearchableEmail == searchable);
        if (user is null) return null;

        return await DecryptUserAsync(user);
    }

    private async Task<(string email, string? phone)> EncryptUserFieldsAsync(
        string email, string? phone)
    {
        var emailTask = _encryption.EncryptAsync(email.ToLowerInvariant().Trim());
        var phoneTask = phone is not null
            ? _encryption.EncryptAsync(phone)
            : Task.FromResult<string?>(null);

        await Task.WhenAll(emailTask, phoneTask);
        return (await emailTask, await phoneTask);
    }

    private async Task<UserDto> DecryptUserAsync(User user)
    {
        var emailTask = _encryption.DecryptAsync(user.EmailCiphertext);
        var phoneTask = user.PhoneNumberCiphertext is not null
            ? _encryption.DecryptAsync(user.PhoneNumberCiphertext)
            : Task.FromResult<string?>(null);

        await Task.WhenAll(emailTask, phoneTask);

        return new UserDto(
            user.Id,
            await emailTask,
            await phoneTask,
            user.FirstName,
            user.LastName,
            user.CreatedAt
        );
    }

    private static string HashEmail(string email)
    {
        var bytes = SHA3_256.HashData(
            Encoding.UTF8.GetBytes(email.ToLowerInvariant().Trim()));
        return Convert.ToHexString(bytes).ToLowerInvariant();
    }
}

Registrar en Program.cs:

builder.Services.AddQuantumApiClient(options =>
{
    options.ApiKey = builder.Configuration["QuantumApi:ApiKey"]!;
});

builder.Services.AddScoped<IEncryptionService, QuantumApiEncryptionService>();
builder.Services.AddScoped<IUserRepository, UserRepository>();

Migración de EF Core

dotnet ef migrations add EncryptUserFields
dotnet ef database update

La migración renombra Email a EmailCiphertext, añade SearchableEmail (con índice único), y renombra PhoneNumber a PhoneNumberCiphertext.

Para las filas existentes, necesitarás un script de migración de datos para cifrar los valores en texto plano. Eso es específico de tu situación, así que no se incluye aquí, pero sigue el mismo patrón: leer filas, cifrar campos, actualizar.

Lo que ajustamos

El código generado estaba cerca. Dos ajustes:

1. Cifrado en paralelo. La IA cifró email y teléfono de forma secuencial (await encrypt(email); await encrypt(phone);). Lo cambiamos a Task.WhenAll — dos llamadas HTTP en paralelo en vez de una detrás de otra. En latencia p95, secuencial añade ~100ms. En paralelo se queda por debajo de 50ms.

2. SHA3-256 para SearchableEmail. La IA usó SHA-256. Lo cambiamos a SHA3_256.HashData — SHA3 es resistente a computación cuántica (el algoritmo de Grover sobre SHA2 reduce la seguridad efectiva a la mitad, SHA3 fue diseñado para resistir esto). Para el hashing de email la diferencia es menor, pero es un buen hábito.

Template

=== FIELD-LEVEL ENCRYPTION CHECKLIST ===

IDENTIFY FIELDS
[ ] PII: names, email, phone, date of birth, address
[ ] Financial: card numbers (use tokenisation, not encryption), account numbers
[ ] Health: diagnoses, medications, measurements
[ ] Legal: identity document numbers

FOR EACH ENCRYPTED FIELD, DECIDE
[ ] Searchable? → need a deterministic hash (SHA3-256) alongside the ciphertext
[ ] Sortable? → encrypted fields can't be sorted — surface a sortable non-sensitive proxy
[ ] Full-text searchable? → encrypted fields can't be full-text indexed → consider
    storing only on the application side, not in the DB

KEY MANAGEMENT
[ ] Using QuantumAPI EaaS: key never leaves the service ✓
[ ] Using local keys: key must be stored in QuantumVault or Azure Key Vault (not config)

MIGRATION
[ ] Encrypt existing rows before deploying new code
[ ] Keep old plaintext column during transition, then drop it
[ ] Test: verify you can decrypt data written before the migration

PERFORMANCE
[ ] Encrypt fields in parallel (Task.WhenAll)
[ ] For read-heavy paths, consider request-scoped caching of decrypted values
[ ] Measure p99 latency before and after — EaaS adds ~30-80ms per call

Challenge

Antes del artículo 4, piensa en la autenticación. La API de usuarios usa tokens JWT firmados con un secreto simétrico. Ese secreto ahora está en QuantumVault. Mejor que antes.

Pero el propio token sigue siendo el punto débil: si un atacante roba un JWT, puede usarlo hasta que expire. No hay revocación. Y el algoritmo de firma (HS256 o RS256) será vulnerable a ordenadores cuánticos.

El artículo 4 reemplaza la autenticación JWT con QuantumID — un proveedor OIDC que firma tokens con ML-DSA. Tokens que se pueden revocar, con firmas que sobreviven a los ordenadores cuánticos.

Si esta serie te resulta útil, puedes invitarme a un café.

Comments

Loading comments...