gdpr

package
v0.0.0-...-a6a0f27 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package gdpr provides GDPR-compliant crypto-shredding for event-sourced systems.

In event sourcing, events are immutable and append-only. You can't delete them. Crypto-shredding solves this: encrypt PII with per-subject keys, then "forget" a subject by deleting their encryption keys. The encrypted data becomes unrecoverable — mathematically equivalent to deletion.

How it works

  1. Each data subject (user/person) gets their own encryption key
  2. PII fields in events are encrypted with the subject's key on write
  3. On read, PII is decrypted transparently
  4. To forget: delete the subject's key → PII becomes unreadable
  5. Non-PII fields remain intact (event structure preserved)

Multi-subject events

An event may reference multiple subjects (e.g., a transfer between two users). Each subject's PII is encrypted with their own key. Forgetting one subject leaves the other's data intact.

pii.go — Two approaches to PII field declaration for GDPR crypto-shredding.

Approach 1: Struct tags (`pii:"key"`, `pii:"field"`, `pii:"key:group"`, `pii:"field:group"`)

  • Zero boilerplate: just tag your struct fields
  • Reflection-based with sync.Map cache (first scan ~500ns, cached ~50ns)
  • Multi-subject via groups: `pii:"key:sender"` + `pii:"field:sender"`

Approach 2: PIIEvent interface

  • Compile-time safe, no reflection
  • User implements SubjectID() and PIIFields()
  • Slightly more boilerplate but zero runtime cost for field discovery

Both are supported. The EncryptedStore auto-detects which approach an event uses.

Index

Constants

View Source
const ForgottenPlaceholder = "[FORGOTTEN]"

ForgottenPlaceholder is the value used for PII fields of forgotten subjects.

Variables

View Source
var (
	// ErrSubjectNotFound is returned when a subject ID doesn't exist.
	ErrSubjectNotFound = errors.New("gdpr: subject not found")

	// ErrForgotten is returned when trying to read PII for a forgotten subject.
	// Events are still readable — only PII fields are replaced with this sentinel.
	ErrForgotten = errors.New("gdpr: data has been forgotten (crypto-shredded)")

	// ErrSubjectExists is returned when trying to create a duplicate subject.
	ErrSubjectExists = errors.New("gdpr: subject already exists")
)
View Source
var ErrFieldNotString = fmt.Errorf("gdpr: pii-tagged field must be string type")

ErrFieldNotString is returned when a pii-tagged field is not a string.

View Source
var ErrNoPIIFields = fmt.Errorf("gdpr: pii:\"key\" found without any pii:\"field\" in same group")

ErrNoPIIFields is returned when pii:"key" exists without any pii:"field" tags.

View Source
var ErrNoPIIKey = fmt.Errorf("gdpr: pii:\"field\" found without pii:\"key\" in same group")

ErrNoPIIKey is returned when pii:"field" tags exist without a pii:"key" tag.

View Source
var ErrNotStruct = fmt.Errorf("gdpr: type is not a struct")

ErrNotStruct is returned when scanning a non-struct type.

Functions

func ClearSchemaCache

func ClearSchemaCache()

ClearSchemaCache clears the pii tag schema cache. For testing only.

Types

type AuditEntry

type AuditEntry struct {
	SubjectID       string    `json:"subject_id"`
	ForgottenAt     time.Time `json:"forgotten_at"`
	Operator        string    `json:"operator"` // who initiated the forget
	AffectedStreams []string  `json:"affected_streams,omitempty"`
	Reason          string    `json:"reason,omitempty"`
}

AuditEntry records a forget operation for compliance proof.

type AuditLog

type AuditLog interface {
	Record(ctx context.Context, entry AuditEntry) error
	List(ctx context.Context) ([]AuditEntry, error)
	ForSubject(ctx context.Context, subjectID string) ([]AuditEntry, error)
}

AuditLog records all forget operations. Required for GDPR compliance proof.

type EncryptedFields

type EncryptedFields map[string]map[string][]byte

EncryptedFields stores encrypted PII per subject within an event. The key is the subject ID, the value is field name → ciphertext.

func (EncryptedFields) MarshalJSON

func (ef EncryptedFields) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for EncryptedFields.

func (*EncryptedFields) UnmarshalJSON

func (ef *EncryptedFields) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for EncryptedFields.

type EncryptedStore

type EncryptedStore[E any] struct {
	// contains filtered or unexported fields
}

EncryptedStore wraps any EventStore[E] to transparently encrypt/decrypt PII.

Auto-detects PII declaration method:

  1. PIIEvent interface (compile-time safe, no reflection)
  2. PIIMultiSubjectEvent interface (multi-subject, no reflection)
  3. Struct tags: pii:"key", pii:"field" (zero boilerplate, cached reflection)

Events without PII pass through with zero overhead.

func NewEncryptedStore

func NewEncryptedStore[E any](inner eskit.EventStore[E], encryptor *SubjectEncryptor) *EncryptedStore[E]

NewEncryptedStore wraps an EventStore with transparent GDPR PII encryption.

func (*EncryptedStore[E]) Append

func (s *EncryptedStore[E]) Append(ctx context.Context, streamID string, expectedVersion int, events []E, metadata ...eskit.Metadata) ([]eskit.Event[E], error)

Append encrypts PII fields (if present) and delegates to the inner store.

func (*EncryptedStore[E]) Inner

func (s *EncryptedStore[E]) Inner() eskit.EventStore[E]

Inner returns the underlying store.

func (*EncryptedStore[E]) Load

func (s *EncryptedStore[E]) Load(ctx context.Context, streamID string) ([]eskit.Event[E], error)

Load loads events and decrypts PII fields.

func (*EncryptedStore[E]) LoadFrom

func (s *EncryptedStore[E]) LoadFrom(ctx context.Context, streamID string, fromVersion int) ([]eskit.Event[E], error)

LoadFrom loads events from a version and decrypts PII fields.

type ForgetService

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

ForgetService orchestrates the crypto-shred (forget) operation. It deletes the subject's key and records the operation in the audit log.

func NewForgetService

func NewForgetService(keys SubjectKeyStore, audit AuditLog) *ForgetService

NewForgetService creates a new forget service.

func (*ForgetService) Forget

func (s *ForgetService) Forget(ctx context.Context, subjectID, operator, reason string) error

Forget crypto-shreds a subject's data by deleting their encryption key. This is irreversible. The audit log records the operation for compliance.

type MemoryAuditLog

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

MemoryAuditLog is an in-memory audit log for testing.

func NewMemoryAuditLog

func NewMemoryAuditLog() *MemoryAuditLog

func (*MemoryAuditLog) ForSubject

func (l *MemoryAuditLog) ForSubject(_ context.Context, subjectID string) ([]AuditEntry, error)

func (*MemoryAuditLog) List

func (l *MemoryAuditLog) List(_ context.Context) ([]AuditEntry, error)

func (*MemoryAuditLog) Record

func (l *MemoryAuditLog) Record(_ context.Context, entry AuditEntry) error

type MemorySubjectKeyStore

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

MemorySubjectKeyStore is an in-memory SubjectKeyStore for testing.

func NewMemorySubjectKeyStore

func NewMemorySubjectKeyStore() *MemorySubjectKeyStore

func (*MemorySubjectKeyStore) CreateKey

func (s *MemorySubjectKeyStore) CreateKey(_ context.Context, subjectID string) ([]byte, error)

func (*MemorySubjectKeyStore) DeleteKey

func (s *MemorySubjectKeyStore) DeleteKey(_ context.Context, subjectID string) error

func (*MemorySubjectKeyStore) Exists

func (s *MemorySubjectKeyStore) Exists(_ context.Context, subjectID string) (bool, error)

func (*MemorySubjectKeyStore) GetKey

func (s *MemorySubjectKeyStore) GetKey(_ context.Context, subjectID string) ([]byte, error)

func (*MemorySubjectKeyStore) Streams

func (s *MemorySubjectKeyStore) Streams(subjectID string) []string

Streams returns the stream IDs associated with a subject.

func (*MemorySubjectKeyStore) TrackStream

func (s *MemorySubjectKeyStore) TrackStream(subjectID, streamID string)

TrackStream associates a subject with a stream (for audit trails).

type PIICarrier

type PIICarrier interface {
	// SubjectIDs returns the data subject IDs whose PII is in this event.
	SubjectIDs() []string
}

PIICarrier is implemented by events that contain PII. It declares which subject(s) own PII in this event.

type PIIEvent

type PIIEvent interface {
	// SubjectID returns the data subject identifier for this event.
	SubjectID() string

	// PIIFields returns pointers to PII string fields, keyed by field name.
	// The pointers allow in-place read and write (encrypt/decrypt).
	PIIFields() map[string]*string
}

PIIEvent is the interface approach for declaring PII fields. Events implement this for compile-time safety and zero reflection cost.

func (e *OrderCreated) SubjectID() string { return e.CustomerID }
func (e *OrderCreated) PIIFields() map[string]*string {
    return map[string]*string{"email": &e.CustomerEmail, "name": &e.CustomerName}
}

type PIIFields

type PIIFields interface {
	// GetPII returns PII field values keyed by field name.
	GetPII() map[string]string

	// SetPII replaces PII fields. Called with decrypted values on read,
	// or with the ForgottenPlaceholder on forgotten subjects.
	SetPII(fields map[string]string)
}

PIIFields is implemented by events to declare which fields contain PII. Returns a map of field name → plaintext value for encryption.

type PIIGroup

type PIIGroup struct {
	SubjectID string
	Fields    map[string]*string
}

PIIGroup represents one data subject's PII within a multi-subject event.

type PIIMultiSubjectEvent

type PIIMultiSubjectEvent interface {
	PIIGroups() map[string]PIIGroup
}

PIIMultiSubjectEvent extends PIIEvent for events with multiple data subjects. Each group maps to a subject ID and its PII fields.

func (e *Transfer) PIIGroups() map[string]PIIGroup {
    return map[string]PIIGroup{
        "sender":   {SubjectID: e.FromUserID, Fields: map[string]*string{"name": &e.FromName}},
        "receiver": {SubjectID: e.ToUserID, Fields: map[string]*string{"name": &e.ToName}},
    }
}

type SubjectEncryptor

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

SubjectEncryptor handles per-subject AES-256-GCM encryption of PII fields.

func NewSubjectEncryptor

func NewSubjectEncryptor(keys SubjectKeyStore) *SubjectEncryptor

NewSubjectEncryptor creates a new per-subject encryptor.

func (*SubjectEncryptor) DecryptPII

func (e *SubjectEncryptor) DecryptPII(ctx context.Context, subjectID string, fields map[string][]byte) (map[string]string, error)

DecryptPII decrypts PII fields for a subject. Returns decrypted field map. If the subject is forgotten, returns ForgottenPlaceholder for all fields.

func (*SubjectEncryptor) EncryptPII

func (e *SubjectEncryptor) EncryptPII(ctx context.Context, subjectID string, fields map[string]string) (map[string][]byte, error)

EncryptPII encrypts PII fields for a subject. Returns encrypted field map.

type SubjectKeyStore

type SubjectKeyStore interface {
	// CreateKey generates and stores a new key for a subject. Returns the key.
	// Returns ErrSubjectExists if the subject already has a key.
	CreateKey(ctx context.Context, subjectID string) ([]byte, error)

	// GetKey returns the encryption key for a subject.
	// Returns ErrSubjectNotFound if unknown, ErrForgotten if shredded.
	GetKey(ctx context.Context, subjectID string) ([]byte, error)

	// DeleteKey permanently deletes a subject's key (crypto-shred).
	// This is the "forget" operation — irreversible.
	// Returns the subject's stream IDs (if tracked) for audit purposes.
	DeleteKey(ctx context.Context, subjectID string) error

	// Exists checks if a subject has an active (non-forgotten) key.
	Exists(ctx context.Context, subjectID string) (bool, error)
}

SubjectKeyStore manages per-subject encryption keys. Each subject gets a unique AES-256 key for their PII.

Jump to

Keyboard shortcuts

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