Documentation
¶
Overview ¶
Package payloadcrypt seals the sensitive free-text fields of an agent trace (prompt, completion, tool params/results, reasoning) before they leave the emitting process, so the CloudEvent — and the BigQuery row the recorder derives from it — carries ciphertext rather than plaintext.
The scheme is envelope encryption: a fresh AES-256 data-encryption key (DEK) is minted per CloudEvent, the DEK is wrapped by a Cloud KMS symmetric KEK (one KMS Encrypt call per event, regardless of how many fields it seals), and each field is AES-256-GCM-sealed under the DEK with a fresh nonce. Recovering any field requires KMS Decrypt on the KEK (the break-glass second factor, PAM-gated) followed by an AES-GCM open — so a reader holding only BigQuery dataViewer sees ciphertext and nothing more.
The producer side holds encrypt-only (roles/cloudkms.cryptoKeyEncrypter) on the KEK and can never read a payload back. Reader/break-glass code uses Open with an injected unwrap callback, mirroring the GCP-free split in public/sdk/uploads/envelope.go. The Cloud KMS-backed wrap for the producer side lives in the sibling kmsseal package.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DEKWrapAAD ¶
func DEKWrapAAD() []byte
DEKWrapAAD returns a copy of the additional-authenticated-data bound to the KMS DEK wrap, so reader/break-glass code passes the same AAD to KMS Decrypt.
func Open ¶
func Open(envelope []byte, unwrapDEK func(kek string, wrapped []byte) (dek []byte, err error)) ([]byte, error)
Open decrypts a sealed envelope. unwrapDEK recovers the AES DEK from the KMS-wrapped DEK — typically a KMS Decrypt against kek (the envelope's declared Sealed.KEK) with DEKWrapAAD. kek is passed to the callback so a reader spanning envelopes minted under different keys (e.g. across KEK rotation, all living in the same dataset) can Decrypt against the right key without re-parsing the envelope. Open stays GCP-free so tests and readers can reuse the AES-GCM/base64 logic without pulling in cloud.google.com/go/kms.
Types ¶
type Encryptor ¶
type Encryptor struct {
// contains filtered or unexported fields
}
Encryptor mints per-event sealing sessions. Safe for concurrent use.
Example ¶
ExampleEncryptor shows the producer (seal) and reader (open) halves against a stub DEK-wrap. Production wraps the DEK via Cloud KMS — see kmsseal.New — but the seal/open round-trip is otherwise identical and GCP-free.
package main
import (
"context"
"fmt"
"chainguard.dev/driftlessaf/agents/agenttrace/payloadcrypt"
)
func main() {
const keyName = "projects/p/locations/us-central1/keyRings/argos/cryptoKeys/agent-trace-payload-key"
// Stub wrap/unwrap standing in for a KMS Encrypt/Decrypt under the KEK.
// Real deployments use kmsseal.New for wrap and a PAM-gated KMS Decrypt for
// unwrap; the DEK never leaves KMS in the clear on the producer side.
wrap := func(_ context.Context, dek []byte) ([]byte, error) { return dek, nil }
unwrap := func(_ string, wrapped []byte) ([]byte, error) { return wrapped, nil }
enc, err := payloadcrypt.New(keyName, wrap)
if err != nil {
panic(err)
}
// One session per CloudEvent: every field it seals shares a single wrapped
// DEK, so there is exactly one KMS call per event.
sess, err := enc.NewSession(context.Background())
if err != nil {
panic(err)
}
sealed, err := sess.Seal([]byte(`"analyze CVE-2025-1234"`))
if err != nil {
panic(err)
}
// Reader/break-glass side recovers the plaintext.
plaintext, err := payloadcrypt.Open(sealed, unwrap)
if err != nil {
panic(err)
}
fmt.Println(string(plaintext))
}
Output: "analyze CVE-2025-1234"
type Sealed ¶
type Sealed struct {
// Enc is the envelope-version marker; also lets consumers detect ciphertext.
Enc string `json:"driftlessaf_enc"`
// KEK is the KMS crypto-key resource name whose Decrypt recovers the DEK.
KEK string `json:"kek"`
// WrappedDEK is the base64 KMS-wrapped per-event AES-256 DEK.
WrappedDEK string `json:"wdek"`
// IV is the base64 AES-GCM nonce for this field.
IV string `json:"iv"`
// Ciphertext is the base64 AES-GCM ciphertext (including the auth tag).
Ciphertext string `json:"ct"`
}
Sealed is the JSON wire shape written in place of a sensitive field. All binary fields are base64. The recorder loads it as native JSON (JSON columns) or as a JSON string (STRING columns); either way it is opaque to BigQuery.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session seals many fields under a single KMS-wrapped DEK — one KMS Encrypt per session. Create one per CloudEvent (via NewSession) so a trace's or span's fields share a DEK and per-event KMS calls stay O(1) regardless of how many tool calls or reasoning blocks a trace contains.