Segurança PII — Criptografia de Campos
O sistema implementa criptografia de campos (field-level encryption) para dados pessoais sensíveis dos funcionários, usando AES-256-GCM com chaves derivadas por tenant.
Visão Geral
| Característica | Detalhe |
|---|---|
| Algoritmo | AES-256-GCM |
| KDF | HKDF-SHA-256 (RFC 5869) |
| Módulo | backend/src/common/crypto/ |
| Serviço KMS | TenantKmsService — tenant-kms.service.ts |
| Primitivos | field-encrypt.ts — encryptField, decryptField, deriveDek |
| Endpoint de rotação | POST /superadmin/tenants/:id/rotate-key |
Campos Criptografados
Employee (backend/src/hr/schemas/employee.schema.ts)
| Campo (MongoDB) | Conteúdo | Campo legado (migração) |
|---|---|---|
cpfEncrypted | CPF do funcionário (11 dígitos) | cpf (string — removido após migração) |
bankAccountEncrypted | JSON com { bank, agency, account, accountType } | bankAccountJson (removido após migração) |
baseSalaryCipher | Salário-base em centavos (stringified number) | baseSalaryCents (número — mantido para cálculo; campo cipher para auditoria segura) |
PayrollLine (backend/src/hr/schemas/payroll-line.schema.ts)
| Campo | Conteúdo |
|---|---|
deductionsCipher | Array PayrollDeduction[] — JSON-stringified + AES-256-GCM |
O array deductions (plaintext) coexiste com deductionsCipher durante a janela de migração. Tenants sem kmsKeySaltCurrent configurado usam apenas o campo plaintext.
Subdocumento EncryptedField
Todos os campos criptografados usam o mesmo subdocumento Mongoose (backend/src/common/crypto/field-encrypt.ts):
{
ciphertext: string; // base64
iv: string; // base64, 12 bytes (GCM nonce único por chamada)
authTag: string; // base64, 16 bytes (GCM auth tag)
alg: string; // 'aes-256-gcm' — para futura rotação de algoritmo
}O IV de 12 bytes é gerado aleatoriamente a cada operação de encrypt (randomBytes(12)). Nunca é reutilizado para a mesma chave.
Derivação de Chave (DEK)
APP_SECRET (env) Tenant.kmsKeySaltCurrent (MongoDB, 32 bytes hex)
↓ ↓
HKDF-SHA-256 (RFC 5869, info: 'popinaflow-hr-pii-v1')
↓
DEK (32 bytes = chave AES-256)- O master key (
APP_SECRET) é o primeiro segredo — 32 caracteres mínimo. - O salt por tenant (
kmsKeySaltCurrent) isola cada DEK. Dois tenants com o mesmoAPP_SECRETtêm DEKs diferentes. - O campo
info(popinaflow-hr-pii-v1) vincula a derivação ao contexto HR, separando futuras DEKs de outros fins.
KMS Bootstrap (lazy)
Tenants criados antes do módulo PII não têm kmsKeySaltCurrent. Na primeira chamada a TenantKmsService.getDek(tenantId):
- O serviço detecta
kmsKeySaltCurrent == null. - Gera um salt aleatório de 32 bytes (
generateKmsSalt()). - Persiste em
Tenant.kmsKeySaltCurrente defineTenant.kmsKeyVersion = 1. - Retorna o DEK derivado.
Esse bootstrap é transparente — sem migração manual necessária.
Rotação de Chave (DEK Rotation)
Endpoint
POST /superadmin/tenants/:id/rotate-key
Authorization: Bearer <superadmin-token>Resposta:
{
"reEncryptedEmployees": 42,
"reEncryptedPayrollLines": 387,
"newKeyVersion": 3
}Algoritmo de rotação (TenantKmsService.rotateKey)
- Gera novo salt (
generateKmsSalt). - Deriva novo DEK a partir do novo salt.
- Em lotes de 100: lê todos os
Employeedo tenant, descriptografa cadaEncryptedFieldcom o DEK atual, recriptografa com o novo DEK, persiste viafindByIdAndUpdate. - Repete para todos os
PayrollLine(campodeductionsCipher). - Atualiza o tenant:
kmsKeySaltPrevious = kmsKeySaltCurrent,kmsKeySaltCurrent = newSalt,kmsKeyVersion++.
O salt anterior (kmsKeySaltPrevious) é retido por um ciclo como fallback de leitura — documentos que não foram re-encriptados a tempo ainda podem ser decriptados enquanto o kmsKeySaltPrevious existe.
Campos do Tenant relacionados ao KMS
| Campo | Tipo | Descrição |
|---|---|---|
kmsKeySaltCurrent | string (hex, 64 chars) | Salt ativo para derivação do DEK |
kmsKeySaltPrevious | string? | Salt anterior (fallback por 1 ciclo) |
kmsKeyVersion | number | Número da versão atual (audit trail) |
Migração de Dados (plaintext → encrypted)
Para tenants com dados legados em cpf plaintext:
POST /superadmin/tenants/:id/migrate-piiO endpoint (TenantKmsService.migrateEmployeesPii) varre Employee que têm cpf preenchido e cpfEncrypted ausente, popula os campos criptografados e nulifica os campos legados. Idempotente — pode ser executado múltiplas vezes com segurança.
Decrypt sob demanda (opt-in)
Por padrão, as rotas HR retornam os campos PII omitidos — cpfEncrypted, bankAccountEncrypted e baseSalaryCipher não aparecem nas respostas. Para obter os valores decriptados, o chamador deve passar ?decrypt=true na rota de detalhe do funcionário (requer role Admin ou Superadmin). O sistema então:
- Busca o DEK via
TenantKmsService.getDek(tenantId). - Chama
decryptField(field, dek)para cadaEncryptedFieldpresente. - Inclui os valores no payload de resposta.
Limitações v1 (env-master-key)
A versão atual usa
APP_SECRET(variável de ambiente) como master key.
Risco: Comprometimento de APP_SECRET expõe os DEKs de todos os tenants simultaneamente (pois todos os DEKs são derivados do mesmo master key).
Caminho de upgrade recomendado: Migrar para um KMS externo real (GCP Cloud KMS ou AWS KMS):
- O master key passa a ser uma referência ao KMS (
projects/.../cryptoKeyVersions/...). deriveDek()substitui ohkdfSynclocal por uma chamada autenticada ao KMS.- O
APP_SECRETdeixa de existir.
O campo alg no EncryptedField suporta rotação de algoritmo sem migração de schema.
Relacionados
- HCM — Recursos Humanos — Schemas que usam EncryptedField
- LGPD — Direito de erasure usa o mesmo mecanismo de decrypt para anonimização
- Superadmin — Tenants — Endpoint
rotate-key