JuliusBrussee/caveman · critical
kms: decode envelope: %w
Error message
kms: decode envelope: %w
What it means
Decrypt first checks IsEnvelope (the 'cave-kms-v1:' prefix) and the 256 KiB envelope size cap, then json.Unmarshals everything after the prefix into Envelope{provider,region,key_id,ciphertext}. This error means the prefix was present but the remaining bytes are not valid Envelope JSON — the envelope is corrupted or was mangled in storage/transit. Provider/region/key checks run only after this decode succeeds.
Source
Thrown at shared/platform/kms/kms.go:231
client, err := FromPayloadEnvironment()
if err != nil {
return nil, err
}
return client.Decrypt(ctx, blob)
}
// Decrypt unwraps versioned KMS envelope. Metadata can choose only validated
// key identity under configured provider; it can never choose host or token.
func (c *Client) Decrypt(ctx context.Context, blob []byte) ([]byte, error) {
if !IsEnvelope(blob) {
return nil, errors.New("kms: unknown envelope format")
}
if len(blob) > maxEnvelopeBytes {
return nil, errors.New("kms: envelope exceeds size limit")
}
var envelope Envelope
if err := json.Unmarshal(blob[len(prefix):], &envelope); err != nil {
return nil, fmt.Errorf("kms: decode envelope: %w", err)
}
if envelope.Provider != c.provider {
return nil, errors.New("kms: envelope provider does not match configured provider")
}
if err := validateLocation(envelope.Region, envelope.KeyID); err != nil {
return nil, err
}
if envelope.Region != c.region {
return nil, errors.New("kms: envelope region is not approved")
}
if _, ok := c.decryptKeyIDs[envelope.KeyID]; !ok {
return nil, errors.New("kms: envelope key ID is not approved")
}
if strings.TrimSpace(envelope.Ciphertext) == "" {
return nil, errors.New("kms: envelope ciphertext is empty")
}
var response struct {
KeyID string `json:"key_id"`View on GitHub (pinned to 27d5a3981a)
Solutions
- Compare the stored envelope against the original Encrypt output byte-for-byte (length check catches truncation)
- Ensure the storage column/type preserves the full string (TEXT/CLOB, no charset rewrites)
- Never embed the envelope in another document without re-encoding the whole envelope afterwards
- If corruption is confirmed, the ciphertext is unrecoverable — re-encrypt the plaintext from its source of truth
Example fix
// before
// stored envelope was truncated by VARCHAR(255)
stored := "cave-kms-v1:{\"provider\":\"scaleway\",\"region\":\"fr-par\"" // cut mid-JSON
// after
// store full envelope in TEXT column
stored := "cave-kms-v1:{\"provider\":\"scaleway\",\"region\":\"fr-par\",\"key_id\":\"...\",\"ciphertext\":\"...\"}" Defensive patterns
Strategy: validation
Validate before calling
func envelopeIntact(blob []byte) bool {
if !kms.IsEnvelope(blob) { return false }
var e kms.Envelope
return json.Unmarshal(blob[len("cave-kms-v1:"):], &e) == nil &&
e.Provider != "" && e.Ciphertext != ""
} Type guard
func isValidEnvelope(blob []byte) bool {
return kms.IsEnvelope(blob) && json.Valid(blob[len("cave-kms-v1:"):])
} Try / catch
if _, err := client.Decrypt(ctx, blob); err != nil { if strings.Contains(err.Error(), "decode envelope") { /* quarantine row; do not retry; re-encrypt from source of truth */ } } Prevention
- Store envelopes in TEXT/CLOB columns sized for growth; verify byte length after write
- Round-trip check (Encrypt then Decrypt) before persisting in a new storage backend
- Never pass envelopes through templates, logs, or manual edits
When it happens
Trigger: Envelope string truncated by a column size limit or copy/paste; base64 or URL-encoding applied to part of the blob; hand-crafted string starting with 'cave-kms-v1:' followed by non-JSON; DB or ORM layer escaping/stripping characters inside the JSON; envelope bytes stored as [BLOB] text by a migration tool.
Common situations: VARCHAR column too short silently truncating on some databases; secrets passed through templates or logs that strip quotes/newlines; double-encoding when the envelope is embedded in another JSON document as raw text; manual repair attempts on production secret rows.
Related errors
- corpus provider conversion supports string or null content
- cachebench: verifier returned duplicate or invalid JSON
- cachebench: verifier returned invalid JSON
- cacheengine: JSON nesting limit exceeded
- cacheengine: request root must be object
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/d12e1027d10e14d2.
Report an issue: GitHub.