Documentation
¶
Overview ¶
Package erasure defines GDPR crypto-erasure entities and store interface.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Erasure ¶
type Erasure struct {
chronicle.Entity
ID id.ID `json:"id"`
SubjectID string `json:"subject_id"`
Reason string `json:"reason"`
RequestedBy string `json:"requested_by"`
EventsAffected int64 `json:"events_affected"`
KeyDestroyed bool `json:"key_destroyed"`
AppID string `json:"app_id"`
TenantID string `json:"tenant_id"`
Status Status `json:"status"`
// LegacyKeyRetained mirrors Result.LegacyKeyRetained.
LegacyKeyRetained bool `json:"legacy_key_retained"`
}
Erasure records a GDPR erasure event.
The record is written before anything irreversible happens, as pending, and completed once the keys are dealt with. Records written before erasures had a status are completed.
type Input ¶
type Input struct {
SubjectID string `json:"subject_id" validate:"required"`
Reason string `json:"reason" validate:"required"`
RequestedBy string `json:"requested_by" validate:"required"`
}
Input is the request to erase a subject's data.
type KeyUsage ¶
type KeyUsage struct {
Scope
EncryptionKeyID string `json:"encryption_key_id"`
Erased bool `json:"erased"`
Events int64 `json:"events"`
}
KeyUsage counts one subject's events that share an app and tenant, the key they were sealed under, and whether they are already marked erased.
type ListOpts ¶
ListOpts defines pagination and scope options for listing erasure records.
Scope is applied by the store, before LIMIT/OFFSET: filtering after pagination would silently hide a caller's own records behind another tenant's.
type Result ¶
type Result struct {
ID id.ID `json:"id"`
SubjectID string `json:"subject_id"`
EventsAffected int64 `json:"events_affected"`
KeyDestroyed bool `json:"key_destroyed"`
// LegacyKeyRetained is set when some of the erased events were sealed
// before keys were scoped, under a key that live events in another app or
// tenant still depend on. The events are marked erased and no read path
// will show their payload, but the key was kept, so KeyDestroyed is false.
// The key goes the first time an erasure finds no other scope still
// holding unerased events under it. See Service.Erase.
LegacyKeyRetained bool `json:"legacy_key_retained,omitempty"`
}
Result is the outcome of an erasure operation.
type Scope ¶
type Scope struct {
AppID string `json:"app_id,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
}
Scope identifies the app and tenant an erasure operation is confined to.
A zero Scope matches every app and tenant. Only trusted in-process callers may use one: erasure records carry subject IDs and request reasons, and MarkErased mutates events, so an unscoped call both leaks and tampers across tenants.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service performs GDPR crypto-erasure operations.
func NewService ¶
NewService creates a new erasure Service.
func (*Service) Erase ¶
Erase performs a GDPR erasure for a subject within the caller's app and tenant:
- Count the subject's events in scope.
- Record the erasure as pending, with KeyDestroyed false.
- Mark the events erased.
- Destroy the keys those events were sealed under, and no others.
- Complete the record with the outcome.
Destroying keys is the one step that cannot be undone, so it runs only once the erasure is on record and the events are marked. If a step fails, Erase returns an error naming the erasure and stops. The record stays pending, and until step 4 the keys are untouched. If step 4 fails part way, the keys that failed to delete survive, the ones that did delete stay deleted, and the record still says KeyDestroyed false. If step 5 fails, the keys are gone but the record still says pending.
A pending record is the signal to run Erase again for the same subject and scope. The retry writes its own record, marks every event again (including any the failed attempt reached), deletes the keys again (deleting a missing key is not an error) and completes. The pending record stays as the trail of the failed attempt.
Everything is confined to the scope. An empty TenantID covers every tenant in the app, and an empty AppID and TenantID together cover everything, which is the rule the stores apply to CountBySubject and MarkErased. Step 4 follows the same rule, so the keys destroyed always belong to exactly the events marked.
Keys sealed before scoping ¶
Events sealed before this release sit under a key named by the bare subject ID, shared by every app and tenant that used that subject ID. Destroying it on behalf of one scope destroys the others' data with no record in their scope, which is the bug scoping fixed. Keeping it means the erasing scope's ciphertext can still be decrypted by anyone holding the key store.
Erase destroys a legacy key only when no other scope still holds unerased events sealed under it. Otherwise it marks the caller's events erased as usual, keeps the key, and completes the record with KeyDestroyed false and LegacyKeyRetained set, which the result reports too. crypto.Sealer.Open redacts any event flagged erased, so the payload stops being readable through Chronicle straight away, but the erasure is not cryptographic until the key goes. The key goes the first time any sharing scope erases the subject and finds every other scope's legacy events already erased, so running Erase again later is how a retained key gets cleaned up.
Which keys to destroy is decided after the events are marked, from a fresh read. Other scopes' legacy events only ever go from unerased to erased, so a retry can move from keeping the legacy key to destroying it, never back.
Re-sealing the caller's events under a scoped key was ruled out. The hash chain covers the sealed bytes, so rewriting them makes every re-sealed event verify as tampered, and retention archives already hold copies of the old ciphertext that re-sealing cannot reach. Refusing the erasure outright was ruled out too: it leaves the data readable and unmarked, which is worse for the data subject, and two tenants sharing a legacy key would each be refused because of the other, forever.
The check only sees live rows. If retention has already archived and deleted another scope's legacy events, destroying the key also makes that archive unreadable. A legacy key is only ever destroyed when the caller has live legacy events of its own, which keeps a scope that never used the key from destroying it.
Each call is one "erase subject" span carrying counts only. The subject ID is personal data and never goes on it, and neither does an error's text: a key store error can quote the key ID, which embeds the subject ID.
type Status ¶
type Status string
Status is how far an erasure got.
const ( // StatusPending means the erasure was recorded but did not finish. Its // events may be marked and some of its keys may be gone, but not every key // it set out to destroy is confirmed destroyed, so KeyDestroyed is false. // Running Erase again for the same subject and scope finishes the job // under a new record, which is the one the events end up pointing at. StatusPending Status = "pending" // StatusCompleted means the events are marked and the key outcome is final: // KeyDestroyed or LegacyKeyRetained says which. StatusCompleted Status = "completed" )
type Store ¶
type Store interface {
// RecordErasure persists an erasure event.
RecordErasure(ctx context.Context, e *Erasure) error
// CompleteErasure writes an erasure's outcome and sets its status to
// StatusCompleted. It returns chronicle.ErrErasureNotFound when no record
// has that ID.
CompleteErasure(ctx context.Context, erasureID id.ID, o Outcome) error
// GetErasure returns an erasure record by ID.
GetErasure(ctx context.Context, erasureID id.ID) (*Erasure, error)
// ListErasures returns erasure records matching opts, scoped before
// pagination.
ListErasures(ctx context.Context, opts ListOpts) ([]*Erasure, error)
// CountErasures returns the number of erasure records in the given scope.
// Callers wanting a total must use this rather than len(ListErasures(...)),
// which is bounded by the list's limit and loads every row to count it.
CountErasures(ctx context.Context, s Scope) (int64, error)
// CountBySubject returns the number of events for a subject within the
// query's scope. Implementations must honour the scope: an unscoped count
// reveals how much data other tenants hold on that subject.
CountBySubject(ctx context.Context, q SubjectQuery) (int64, error)
// MarkErased flags a subject's events as erased within the query's scope
// and points them at erasureID. Implementations must honour the scope: an
// unscoped update lets any caller tamper with every tenant's audit records.
//
// Events already marked are marked again and re-pointed, and count towards
// the result. That is what lets a retry after a failed erasure take over
// every event the failed attempt reached.
MarkErased(ctx context.Context, q SubjectQuery, erasureID id.ID) (int64, error)
// SubjectKeyUsage groups a subject's events by app, tenant, encryption key
// ID and erased flag, across every app and tenant, with a count for each
// group. Events that were never sealed come back with an empty key ID.
//
// It is unscoped on purpose. Before keys were scoped, a subject's events in
// different tenants were sealed under one shared key, and the erasure
// service has to see every scope depending on that key before it can decide
// whether destroying it is safe. That makes the result exactly what
// CountBySubject's scoping exists to hide, so it is for trusted in-process
// use only and must never be returned to a caller.
SubjectKeyUsage(ctx context.Context, subjectID string) ([]KeyUsage, error)
}
Store manages GDPR erasure records.
type SubjectQuery ¶
SubjectQuery identifies a data subject within a scope.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package erasuretest holds checks every erasure.Store implementation must pass, so each backend runs the same scenario against its own engine.
|
Package erasuretest holds checks every erasure.Store implementation must pass, so each backend runs the same scenario against its own engine. |