Documentation
¶
Overview ¶
Package crypto provides AES-256-GCM encryption for GDPR crypto-erasure.
Index ¶
Constants ¶
const ErasedMarker = "[ERASED]"
ErasedMarker replaces a sealed value whose key has been destroyed.
Variables ¶
var ErrAlreadySealed = errors.New("crypto: event is already sealed")
ErrAlreadySealed is returned when sealing an event that already carries ciphertext, which would make the original unrecoverable behind two layers.
var ErrKeyScopeMismatch = errors.New("crypto: event's key ID names a different scope")
ErrKeyScopeMismatch is returned by Sealer.Open when an event's recorded key ID names a different app, tenant or subject than the event itself. EncryptionKeyID is not covered by the hash, so this is how an edited row is caught before it can point a read at another scope's key.
Functions ¶
func ParseScopedKeyID ¶
ParseScopedKeyID reverses ScopedKeyID. ok is false for any string ScopedKeyID could not have produced, which includes every legacy key ID: the bare subject IDs that events were sealed under before keys were scoped.
func ScopedKeyID ¶
ScopedKeyID returns the key store identifier for a subject's key within one app and tenant. Sealer.Seal asks the key store for this ID and records it in Event.EncryptionKeyID, and erasure destroys it.
The scope is part of the identity because subject IDs are not unique across apps and tenants. "user-42" in one tenant is usually a different person from "user-42" in the next, and when the key was addressed by subject alone, erasing one of them destroyed the other's data too.
Each field is length-prefixed ("<byte-length>:<value>"), the same way checkpoint.CanonicalPayload renders its fields. AppID and TenantID are free text, so a bare join lets a separator inside one value move the boundary: joined with ":", app "a:b" with tenant "c" and app "a" with tenant "b:c" would name the same key. With a length in front of each field, a reader finds where it ends without trusting what is inside it.
The result contains ":" and "|" and whatever bytes the IDs themselves hold. A key store that maps IDs onto paths or resource names has to escape or hash them, and treat the ID as opaque.
Types ¶
type Encryptor ¶
type Encryptor struct{}
Encryptor provides AES-256-GCM envelope encryption.
type InMemoryKeyStore ¶
type InMemoryKeyStore struct {
// contains filtered or unexported fields
}
InMemoryKeyStore is an in-memory key store for testing.
func NewInMemoryKeyStore ¶
func NewInMemoryKeyStore() *InMemoryKeyStore
NewInMemoryKeyStore creates a new InMemoryKeyStore.
func (*InMemoryKeyStore) Delete ¶
func (ks *InMemoryKeyStore) Delete(keyID string) error
func (*InMemoryKeyStore) GetOrCreate ¶
func (ks *InMemoryKeyStore) GetOrCreate(keyID string) (retKey []byte, retKeyID string, retErr error)
type KeyStore ¶
type KeyStore interface {
// GetOrCreate returns the key stored under keyID, creating one if none
// exists. The returned ID is informational: Sealer records keyID itself,
// because keyID is what a later Get has to ask for.
GetOrCreate(keyID string) ([]byte, string, error)
// Get returns the key stored under keyID, or ErrErasureKeyNotFound.
Get(keyID string) ([]byte, error)
// Delete destroys the key stored under keyID, making everything sealed
// with it irrecoverable. Deleting a key that does not exist is not an error.
Delete(keyID string) error
}
KeyStore holds the encryption keys crypto-erasure destroys.
Every method takes an opaque key ID. Sealer builds it with ScopedKeyID, so each (app, tenant, subject) gets its own key and erasing a subject in one scope cannot touch another. Implementations must store and compare the ID as given, byte for byte, and must not parse it or assume it is a subject ID.
Events sealed before keys were scoped used the bare subject ID as the key ID. Those keys live in the same store under that name, and Sealer.Open still asks for them by it, so an existing durable key store keeps working across the upgrade without moving a single key. See the erasure package for what erasing such a key does when more than one scope depends on it.
The ID space is shared between the two forms. That is safe because a scoped ID always starts with "ck1|" followed by a length, and nothing new is ever sealed under a legacy ID.
type Sealer ¶
type Sealer struct {
// contains filtered or unexported fields
}
Sealer encrypts and decrypts the personal payload of an audit event using a key per subject within each app and tenant (see ScopedKeyID). That is what makes crypto-erasure work: destroy the key and the payload is unrecoverable while the event's shape and hash chain survive.
Sealed fields are Metadata, Reason and IP. Deliberately left in plaintext:
- SubjectID, because it is the lookup key used to find a subject's events in order to erase them, and to prove afterwards that they were erased.
- UserID, because it identifies the actor rather than the data subject, and because the per-user index and ByUser lookups depend on it.
- Action, Resource, Category, Outcome, Severity, ResourceID and the timestamps, because they are the operational record that must outlive an erasure, and queries and compliance reports group on them.
So an erasure destroys what was recorded about a subject, not the fact that an event involving them occurred. That is the intended posture for an audit log, and callers should not read more into it than that.
func (*Sealer) Open ¶
Open decrypts a sealed event in place.
When the subject's key has been destroyed, the sealed fields are replaced with ErasedMarker and the event is flagged Erased, rather than returning an error: a destroyed key is the expected end state of an erasure, not a failure, and the remaining record still has to be readable.
An event already flagged Erased is redacted the same way without touching the key. Its key can outlive the erasure when a legacy key is shared with another scope (see the erasure package), and an erased event must never display its payload whether or not the key is still around.
Open must not be used on events destined for hash verification. The stored bytes are what the digest covers, so a verifier has to see the sealed form.
func (*Sealer) Seal ¶
Seal encrypts the event's personal payload in place and records which key was used.
It is a no-op for an event with no SubjectID: there is no subject to key the encryption to, and a key that is never destroyed protects nothing.
Callers must Seal before computing the event's hash. Hashing the plaintext would make the chain unverifiable the moment a key is destroyed, because the original bytes needed to recompute the digest would be gone.