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 ¶
- Each data subject (user/person) gets their own encryption key
- PII fields in events are encrypted with the subject's key on write
- On read, PII is decrypted transparently
- To forget: delete the subject's key → PII becomes unreadable
- 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
- Variables
- func ClearSchemaCache()
- type AuditEntry
- type AuditLog
- type EncryptedFields
- type EncryptedStore
- func (s *EncryptedStore[E]) Append(ctx context.Context, streamID string, expectedVersion int, events []E, ...) ([]eskit.Event[E], error)
- func (s *EncryptedStore[E]) Inner() eskit.EventStore[E]
- func (s *EncryptedStore[E]) Load(ctx context.Context, streamID string) ([]eskit.Event[E], error)
- func (s *EncryptedStore[E]) LoadFrom(ctx context.Context, streamID string, fromVersion int) ([]eskit.Event[E], error)
- type ForgetService
- type MemoryAuditLog
- type MemorySubjectKeyStore
- func (s *MemorySubjectKeyStore) CreateKey(_ context.Context, subjectID string) ([]byte, error)
- func (s *MemorySubjectKeyStore) DeleteKey(_ context.Context, subjectID string) error
- func (s *MemorySubjectKeyStore) Exists(_ context.Context, subjectID string) (bool, error)
- func (s *MemorySubjectKeyStore) GetKey(_ context.Context, subjectID string) ([]byte, error)
- func (s *MemorySubjectKeyStore) Streams(subjectID string) []string
- func (s *MemorySubjectKeyStore) TrackStream(subjectID, streamID string)
- type PIICarrier
- type PIIEvent
- type PIIFields
- type PIIGroup
- type PIIMultiSubjectEvent
- type SubjectEncryptor
- type SubjectKeyStore
Constants ¶
const ForgottenPlaceholder = "[FORGOTTEN]"
ForgottenPlaceholder is the value used for PII fields of forgotten subjects.
Variables ¶
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") )
var ErrFieldNotString = fmt.Errorf("gdpr: pii-tagged field must be string type")
ErrFieldNotString is returned when a pii-tagged field is not a string.
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.
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.
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 ¶
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:
- PIIEvent interface (compile-time safe, no reflection)
- PIIMultiSubjectEvent interface (multi-subject, no reflection)
- 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.
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.
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) DeleteKey ¶
func (s *MemorySubjectKeyStore) DeleteKey(_ context.Context, subjectID string) 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 PIIMultiSubjectEvent ¶
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.
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.