gdpr

package
v1.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

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

It is the Go reference other SDKs mirror, so the contract is deliberately tight:

  • The envelope stays FROZEN (GR-1). Protect 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 can't read the protected fields). The which-fields-are-sensitive fact 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 stdlib-only. The core module pulls no crypto/KMS dependency.

The sensitive paths come from the SAME per-URN schema.Schema the produce/consume validation path already loads (via a schema.Provider) — the x-gdpr-sensitive marks ride on it (schema.Schema.SensitivePaths). Protect/Unprotect are standalone helpers the caller invokes, so it is strictly opt-in: a producer/consumer that never calls them behaves exactly as before.

Typical wiring (producer):

env, _ := babelqueue.Make(urn, data, babelqueue.WithQueue("orders"))
sch, ok, _ := provider.Schema(env.URN())
if ok {
    _ = schema.Check(provider, env.URN(), env.Data) // optional: validate cleartext first
    _ = gdpr.Protect(env.Data, sch, cipher)          // encrypt marked leaves IN PLACE
}
body, _ := env.Encode()                              // ciphertext rides inside data

and the inverse on the consumer, after Decode and before the handler reads env.Data:

in, _ := babelqueue.Decode(body)
sch, ok, _ := provider.Schema(in.URN())
if ok {
    _ = gdpr.Unprotect(in.Data, sch, cipher)         // decrypt marked leaves IN PLACE
}

Validate cleartext BEFORE Protect / AFTER Unprotect — a schema that constrains a sensitive field (minLength, enum, …) would reject the ciphertext string otherwise.

Index

Constants

This section is empty.

Variables

View Source
var ErrDecrypt = errors.New("babelqueue/gdpr: cannot decrypt a protected field")

ErrDecrypt wraps any failure to restore a protected field on the consume side (a wrong key, a tampered/garbled ciphertext, or a value that is not the string Protect produced). Unprotect stops at the first such failure and returns it wrapped so it is distinguishable from a missing field (which is skipped, not an error). Detect it with errors.Is.

View Source
var ErrInvalidKeySize = errors.New("babelqueue/gdpr: AES key must be 16, 24, or 32 bytes")

ErrInvalidKeySize is returned by NewAESGCMCipher when the key is not 16, 24, or 32 bytes (AES-128/192/256). Detect it with errors.Is.

View Source
var ErrMalformedCiphertext = errors.New("babelqueue/gdpr: malformed ciphertext")

ErrMalformedCiphertext is returned by AESGCMCipher.Decrypt when the input is not valid base64 or is too short to contain a nonce — i.e. not something this cipher produced. Detect it with errors.Is.

Functions

func Protect

func Protect(data map[string]any, sch *schema.Schema, cipher Cipher) error

Protect 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'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 may be nil (no-op). A nil schema or one with no marks is a no-op (nothing is sensitive). Container marks (a whole object/array marked sensitive) are supported: the entire sub-value is encoded and encrypted as one ciphertext string.

On any cipher error Protect returns it wrapped and leaves data in a partially-protected state; callers should treat a Protect error as fatal for that message (do not publish it).

func Unprotect

func Unprotect(data map[string]any, sch *schema.Schema, cipher Cipher) error

Unprotect is the consumer-side inverse of Protect: it 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 that the cipher cannot open (wrong key, tampered, or not a ciphertext) returns ErrDecrypt, wrapped — the consumer should fail the message (retry / dead-letter) rather than process unreadable PII.

Types

type AESGCMCipher

type AESGCMCipher struct {
	// contains filtered or unexported fields
}

AESGCMCipher is a reference Cipher built ONLY on the Go standard library (crypto/aes + crypto/cipher + crypto/rand): AES-GCM authenticated encryption with a fresh random nonce per call, the nonce prepended to the ciphertext, the whole thing base64 (std, padded) encoded so it drops straight into a JSON string. The key is the CALLER's — this type performs no key management, rotation or derivation; bind a KMS-backed Cipher for that.

AES-256-GCM is used when the key is 32 bytes (the recommended size); 16- and 24-byte keys select AES-128/192-GCM. GCM authenticates the ciphertext, so Decrypt rejects any tampered or wrong-key input with an error (it does not return corrupt plaintext). It is zero-dependency and safe for concurrent use (the cipher.AEAD is created once and only read).

func NewAESGCMCipher

func NewAESGCMCipher(key []byte) (*AESGCMCipher, error)

NewAESGCMCipher builds an AES-GCM reference cipher from a raw symmetric key. The key length selects the AES variant: 32 bytes → AES-256-GCM (recommended), 24 → AES-192, 16 → AES-128. Any other length returns ErrInvalidKeySize.

func (*AESGCMCipher) Decrypt

func (c *AESGCMCipher) Decrypt(ciphertext string) ([]byte, error)

Decrypt reverses Encrypt: it base64-decodes, splits off the prepended nonce, and opens the GCM ciphertext. A wrong key or tampered input fails GCM authentication and returns an error (never corrupt plaintext). Implements Cipher.

func (*AESGCMCipher) Encrypt

func (c *AESGCMCipher) Encrypt(plaintext []byte) (string, error)

Encrypt seals plaintext with a fresh random nonce, prepends the nonce, and base64-encodes the result. Implements Cipher.

type Cipher

type Cipher interface {
	// Encrypt protects one field value (its canonical JSON bytes) and returns a JSON-safe
	// ciphertext string.
	Encrypt(plaintext []byte) (string, error)
	// Decrypt reverses Encrypt, returning the original field-value JSON bytes.
	Decrypt(ciphertext string) ([]byte, error)
}

Cipher is the field-level protection primitive that the CALLER provides — a seam onto a KMS, Vault transit, an HSM, a tokenisation service, or the reference AESGCMCipher below. Protect runs Encrypt over every x-gdpr-sensitive leaf's value (after it is canonically JSON-encoded); Unprotect runs Decrypt to restore it. Keeping this an interface is what holds GR-7: the core module never pulls a crypto/KMS dependency — only a caller who imports a concrete backend does.

Contract for an implementation:

  • Encrypt takes the canonical JSON bytes of one field value (see Protect) and returns the ciphertext as a STRING — it MUST be valid for placement inside a JSON document (the AESGCMCipher reference returns base64, which is). The same plaintext MAY encrypt to different strings each call (a random nonce/IV is expected and good).
  • Decrypt is the exact inverse: given a string Encrypt produced, it returns the original JSON bytes byte-for-byte. A string it did not produce, or one produced under a different key, MUST return an error rather than silent garbage, so a wrong-key consume fails loudly.
  • Both MUST be safe for concurrent use; a producer/consumer fans the same Cipher across goroutines.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL