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

  1. Compare the stored envelope against the original Encrypt output byte-for-byte (length check catches truncation)
  2. Ensure the storage column/type preserves the full string (TEXT/CLOB, no charset rewrites)
  3. Never embed the envelope in another document without re-encoding the whole envelope afterwards
  4. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/d12e1027d10e14d2. Report an issue: GitHub.