Class Gdpr

java.lang.Object
com.babelqueue.gdpr.Gdpr

public final class Gdpr extends Object
The RUNTIME half of ADR-0030: SDK-side field-level encryption of the data fields a registry declared x-gdpr-sensitive. babelqueue-registry only DECLARES and AUDITS sensitivity (and offers a one-way mask for safe logging); this class ENFORCES it on the wire — a producer encrypts each marked leaf before publish, a consumer decrypts it after decode.

The contract mirrors the Go reference so every SDK round-trips byte-for-byte:

  • The envelope stays frozen (GR-1). protect(java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>, com.babelqueue.gdpr.Cipher) mutates only values inside data: a sensitive leaf's value becomes a ciphertext String. It never adds, renames, removes or retypes an envelope field; meta.schema_version stays 1; trace_id is untouched (GR-4). data remains pure JSON (GR-3) — a JSON string is still pure JSON, so any SDK can carry the envelope even without the key (it just cannot read the protected fields). Which fields are sensitive lives in the schema, not the message, so nothing about the frame changes.
  • Zero heavy dependencies (GR-7). The crypto is a caller-provided Cipher interface (KMS/Vault/HSM/tokenisation); the bundled AesGcmCipher is JDK-only. The core pulls no crypto/KMS dependency.

The sensitive paths come from the SAME per-URN schema the produce/consume validation path already loads (via a SchemaProvider) — the x-gdpr-sensitive marks ride on it (SensitivePaths.of(java.util.Map<java.lang.String, java.lang.Object>)). protect(java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>, com.babelqueue.gdpr.Cipher)/unprotect(java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>, com.babelqueue.gdpr.Cipher) are standalone helpers the caller invokes, so the feature is strictly opt-in: a producer/consumer that never calls them behaves exactly as before.

Typical wiring (producer), after building data and before encode/publish:


 Map<String, Object> schema = provider.schemaFor(envelope.job());
 if (schema != null) {
     SchemaValidation.validate(provider, envelope.job(), envelope.data()); // validate cleartext
     Gdpr.protect(envelope.data(), schema, cipher);                        // encrypt marked leaves
 }
 String body = EnvelopeCodec.encode(envelope);                            // ciphertext rides in data
 
and the inverse on the consumer, after decode and before the handler reads data:

 Map<String, Object> schema = provider.schemaFor(in.job());
 if (schema != null) {
     Gdpr.unprotect(in.data(), schema, cipher);                           // decrypt marked leaves
 }
 

Validate cleartext BEFORE protect(java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>, com.babelqueue.gdpr.Cipher) / AFTER unprotect(java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>, com.babelqueue.gdpr.Cipher) — a schema that constrains a sensitive field (minLength, enum, …) would reject the ciphertext string otherwise.

  • Method Details

    • protect

      public static void protect(Map<String,Object> data, Map<String,Object> schema, Cipher cipher)
      Encrypts, in place, every value in data located at a path the schema marked x-gdpr-sensitive — the producer-side step, run after building data and before encode/publish. Each marked leaf's value is canonically JSON-encoded and replaced by Cipher.encrypt(byte[])'s ciphertext String; the envelope frame, non-sensitive fields, and key order are untouched (GR-1).

      A marked path that is absent from data is skipped (not an error) — schemas evolve and a message need not carry every optional field. data/schema/cipher being null is a no-op. A container mark (a whole object/array marked sensitive) is supported: the entire sub-value is encoded and encrypted as one ciphertext string.

      On any cipher failure it throws and leaves data in a partially-protected state; a caller should treat a thrown protect as fatal for that message (do not publish it).

      Parameters:
      data - the message data, mutated in place
      schema - the decoded JSON Schema carrying the x-gdpr-sensitive marks
      cipher - the caller's encryption primitive
    • unprotect

      public static void unprotect(Map<String,Object> data, Map<String,Object> schema, Cipher cipher)
      The consumer-side inverse of protect(java.util.Map<java.lang.String, java.lang.Object>, java.util.Map<java.lang.String, java.lang.Object>, com.babelqueue.gdpr.Cipher): decrypts, in place, every value in data at an x-gdpr-sensitive path, restoring the original JSON value byte-for-byte. Run it after decode and before the handler reads data.

      An absent path is skipped. A leaf that is NOT a string — e.g. it was never protected, or this is a re-run after a successful unprotect — is left as-is, so re-invoking unprotect on already-cleartext data is safe (idempotent for non-string leaves). A string the cipher cannot open (wrong key, tampered, or not a ciphertext) throws a DecryptException — the consumer should fail the message (retry / dead-letter) rather than process unreadable PII.

      Parameters:
      data - the message data, mutated in place
      schema - the decoded JSON Schema carrying the x-gdpr-sensitive marks
      cipher - the caller's encryption primitive
      Throws:
      DecryptException - when a protected field cannot be restored