Skip to content

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ísticaDetalhe
AlgoritmoAES-256-GCM
KDFHKDF-SHA-256 (RFC 5869)
Módulobackend/src/common/crypto/
Serviço KMSTenantKmsServicetenant-kms.service.ts
Primitivosfield-encrypt.tsencryptField, decryptField, deriveDek
Endpoint de rotaçãoPOST /superadmin/tenants/:id/rotate-key

Campos Criptografados

Employee (backend/src/hr/schemas/employee.schema.ts)

Campo (MongoDB)ConteúdoCampo legado (migração)
cpfEncryptedCPF do funcionário (11 dígitos)cpf (string — removido após migração)
bankAccountEncryptedJSON com { bank, agency, account, accountType }bankAccountJson (removido após migração)
baseSalaryCipherSalá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)

CampoConteúdo
deductionsCipherArray 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):

typescript
{
  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 mesmo APP_SECRET tê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):

  1. O serviço detecta kmsKeySaltCurrent == null.
  2. Gera um salt aleatório de 32 bytes (generateKmsSalt()).
  3. Persiste em Tenant.kmsKeySaltCurrent e define Tenant.kmsKeyVersion = 1.
  4. 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:

json
{
  "reEncryptedEmployees": 42,
  "reEncryptedPayrollLines": 387,
  "newKeyVersion": 3
}

Algoritmo de rotação (TenantKmsService.rotateKey)

  1. Gera novo salt (generateKmsSalt).
  2. Deriva novo DEK a partir do novo salt.
  3. Em lotes de 100: lê todos os Employee do tenant, descriptografa cada EncryptedField com o DEK atual, recriptografa com o novo DEK, persiste via findByIdAndUpdate.
  4. Repete para todos os PayrollLine (campo deductionsCipher).
  5. 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

CampoTipoDescrição
kmsKeySaltCurrentstring (hex, 64 chars)Salt ativo para derivação do DEK
kmsKeySaltPreviousstring?Salt anterior (fallback por 1 ciclo)
kmsKeyVersionnumberNú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-pii

O 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 omitidoscpfEncrypted, 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:

  1. Busca o DEK via TenantKmsService.getDek(tenantId).
  2. Chama decryptField(field, dek) para cada EncryptedField presente.
  3. 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 o hkdfSync local por uma chamada autenticada ao KMS.
  • O APP_SECRET deixa de existir.

O campo alg no EncryptedField suporta rotação de algoritmo sem migração de schema.

Relacionados

Lançado sob a licença MIT.