Aller au contenu

Story AUDIT-001c: PiiHasher + per-tenant salt provisioning (envelope encryption)

Module: audit-log Slice: AUDIT-001 (Architecture §11, split part c of a/b/c) Side: [BACKEND] Version target: V0.0.0 Priority: 4 Depends on: AUDIT-001a (audit_tenant_salt table + roles exist), AUDIT-001b (AuditEventBuilder Hashed<...> field type exists), TENANT-001 (TenantOnboarded domain event exists) Can develop concurrently with: AUDIT-001b (different files; both merge after 001a) Merge order: After AUDIT-001a Estimated complexity: M PRD User Stories: AUDIT-US-06 (pseudonymised PII in audit payloads) Wireframe: N/A — backend only


Objective

Provide the PiiHasher bean — the single hashing primitive every producer uses to turn raw assuré PII (phone, name, email, DOB, ID-document number) into a deterministic, per-tenant-salted hash before it ever reaches an audit payload. Provision one secret salt per tenant at onboarding, stored with envelope encryption (a per-tenant data-encryption key wrapped by a platform key) following the D8 pattern. This honours law n°2013-450 minimisation while still letting platform admin reconstruct a case by hashing a candidate value and searching for it.


Backend Scope

Entities

  • Writes rows into audit_tenant_salt (table created in AUDIT-001a): tenant_id PK, salt_ciphertext BYTEA, wrapped_dek BYTEA, kek_version SMALLINT (=1 in V0), created_at, rotated_at (NULL in V0 — rotation is V2).

Migrations

None new (the table ships in AUDIT-001a). This story populates rows at runtime.

Service Layer

All under root package com.altarys.papillon.pcs.controlplane.audit: - PiiHasher (named-interface bean, ...audit.api) — the only callable hashing surface. String hash(String value): returns a hex SHA-256 of salt_bytes || UTF-8(canonical_form(value)). canonical_form lowercases + strips whitespace for emails, normalises phones to E.164. Producers receive a hex string and wrap it in the Hashed<...> type the AuditEventBuilder requires (AUDIT-001b). The hasher is consumed in the hot path from v0.0.0 onward. - PiiHasherImpl (...audit.internal) — holds an in-memory per-tenant salt cache (TTL AUDIT_SALT_CACHE_TTL_SECONDS, default 300 s). On cache miss: SELECT salt_ciphertext + wrapped_dek for the current tenant, unwrap the DEK with the KEK (env AUDIT_KEK), AES-GCM-decrypt the salt, cache it. First call per tenant per JVM ~5–8 ms; cached calls ~10 µs (Architecture §8.1). - TenantSaltProvisioner (...audit.internal) — @ApplicationModuleListener(TenantOnboarded.class). On a TenantOnboarded domain event: SecureRandom.nextBytes(32) → raw salt; SecureRandom.nextBytes(32) → DEK; AES-GCM-encrypt the salt with the DEK → salt_ciphertext; AES-KW-wrap the DEK with the KEK → wrapped_dek; INSERT the audit_tenant_salt row with kek_version = 1; zero the raw salt + DEK in the heap (Architecture §6.4).

API Endpoints

None.

Validation Rules

  • FR-03 (pseudonymisation at write): a raw assuré PII value (phone, name, email, DOB, ID-document number) must be replaced with hash_v1(value, salt[tenant_id]) before persistence. In V0 the producer is responsible for hashing via PiiHasher; the AuditEventBuilder Hashed<...> field type makes hashing the only easy path (Risk R9). The defensive scan at the AUDIT INSERT boundary (which refuses a string that looks like a raw phone/IBAN/email) is out of V0 — deferred to V1 (AUDIT-008).
  • The salt is never persisted in cleartext, never logged, never put in an error envelope, never put in an export (AC-06.2).

Multi-Tenant Considerations

  • Exactly one salt row per tenant; salts differ per tenant, so the same phone number hashes differently across tenants (verified by the canonical isolation test, Architecture §13.2). This is itself a tenant-isolation guarantee: a hash from tenant A cannot be matched against tenant B's audit rows.
  • audit_tenant_salt lives on the platform DB for every tenant including BYO (AUDIT-ADR-01).
  • The KEK (AUDIT_KEK) is a single platform-level secret in V0 (env var). Per-tenant DEKs are wrapped by it. V1+ may move the KEK to a hosted KMS via the same envelope-encryption library.

Audit & Logging

  • No new catalog event is emitted by this story. (Salt rotation would emit a V2-only event_type; not in V0.)

Frontend Scope

N/A — backend only.


Acceptance Criteria

AC-06.1: Allowed PII columns are hashed
Given an audit payload that would carry assuré personal data (full name, phone, email, DOB, ID-document number)
When it is built
Then the raw value is replaced with hash_v1(value, salt_tenant) and the corresponding customer_id is carried when known

AC-06.2: Salt is per-tenant and server-secret
Given the hashing salt
Then it is a per-tenant salt held in platform-managed secret storage (envelope-encrypted), and it never appears in source, in any log line, in any error envelope, or in any export

AC-06.5: Reconstruction works
Given a candidate phone value
When platform admin hashes it with salt_tenant and searches the audit_log
Then any matching event surfaces; matching is exact, not fuzzy

Compliance Rules

  • Law n°2013-450 (CI data-protection) + ARTCI minimisation — pseudonymisation via deterministic hash + per-tenant server-secret salt is the V1 compensating control for assuré PII (AC-06, NFR-09). Status: TV-04 (temporarily validated; lawyer re-research pending) — [LEGAL-REVIEW] applies to the lawful-basis claim.
  • AC-06.3 / AC-06.4 (producer-side conventions, not enforced here): operator actor_user_id (Keycloak sub) is stored cleartext as a stable id; operator name/email are NOT put in the payload (joinable from AUTH at read time). client_ip and user_agent are PII but kept cleartext under the §16.5 proof lawful basis. Producers must honour these; AUDIT only provides the hashing primitive.
  • Salt rotation + cryptographic erasure ("forget" a tenant by burning the salt) is V2 (PRD §A.6 OQ-02).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — named-interface beans, @ApplicationModuleListener, domain-event naming (TenantOnboarded, past tense, no Event suffix).
  • docs/standards/multi-tenant-model.md — per-tenant secret isolation.
  • docs/standards/compliance-discipline.md[LEGAL-REVIEW] for TV-04.
  • docs/standards/critical-rules.md — PII minimisation rules.
  • Background: docs/kb/audit-salt-storage-options.md (rationale for the salt-store choice — human-only KB, do not read programmatically).

Testing Requirements

Unit Tests

  • canonical_form normalises emails (lowercase, trim) and phones (E.164).
  • hash() is deterministic for the same (value, salt) across two JVM invocations (Architecture §13 PiiHasher determinism).
  • Wrapping/unwrapping a DEK with the KEK round-trips; decrypting the salt round-trips.

Integration Tests

  • TenantSaltProvisioner creates exactly one audit_tenant_salt row on a TenantOnboarded event.
  • AC-06.5 reconstruction: emit an event with a hashed phone, then hash the same raw phone and find the row by exact match.
  • Tenant isolation (canonical): the same phone hashed under tenant A vs tenant B yields different hashes (Architecture §13.2).
  • Salt never leaks: scan log output + error envelopes in a forced-failure test; assert no salt bytes present.

What QA Will Validate

  • One salt per tenant, created at onboarding, never in cleartext anywhere.
  • Hash determinism enables exact-match reconstruction.
  • Cross-tenant hashes never collide for the same input.

Out of Scope

  • Defensive PII scan at the AUDIT INSERT boundary (V1, AUDIT-008).
  • Salt rotation, kek_version bump, cryptographic erasure (V2).
  • Moving the KEK to a hosted KMS (V1+).
  • Any producer's decision about WHAT to hash — that lives in each producer story; AUDIT only provides PiiHasher.

Definition of Done

  • [ ] PiiHasher bean exposed as a Modulith named-interface, consumable by every producer
  • [ ] Per-tenant salt provisioned on TenantOnboarded (envelope-encrypted: DEK wrapped by KEK)
  • [ ] Hash is deterministic across JVM restarts (AC-06.5 reconstruction verified)
  • [ ] Salt never persisted in cleartext / logged / in error envelope / in export (AC-06.2 verified)
  • [ ] Same input hashes differently per tenant (tenant isolation verified)
  • [ ] audit_tenant_salt on platform DB for both V1 DB profiles
  • [ ] [LEGAL-REVIEW] note on TV-04 lawful basis retained
  • [ ] No silent failures: a missing/invalid AUDIT_KEK fails fast; a missing salt row for a tenant raises a clear error rather than writing raw PII