JuliusBrussee/caveman · error

envelope: marshal metadata

Error message

envelope: marshal metadata: %w

What it means

Seal generates a random data key, encrypts the payload, then serializes the Metadata struct (scheme, base64 wrapped key, scope hash) to JSON. This error wraps a json.Marshal failure on that Metadata struct. In practice it is nearly impossible to hit because Metadata contains only marshalable types, but the guard keeps Seal fail-closed instead of returning corrupted envelopes.

Solutions

  1. Inspect the wrapped underlying error; it names the exact json.Marshal failure and offending value
  2. Check any recent change to the Metadata struct for fields that are not JSON-marshalable or have an erroring MarshalJSON method
  3. Re-run Seal on a known-good plaintext/key pair to confirm it is data-independent (i.e. a build/code problem, not input)
  4. Report upstream if Metadata is unmodified — this indicates an internal invariant violation

Example fix

// before
type Metadata struct {
	Scheme        string
	WrappedDataKey string
	ScopeHash     string
	Notify        func() // not JSON-marshalable
}
// after
type Metadata struct {
	Scheme         string
	WrappedDataKey string
	ScopeHash      string
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Metadata contains only string fields, so marshal failures are near-impossible;
// guard the envelope before storing:
if _, _, err := envelope.SealForScope(plain, scope); err != nil {
	return fmt.Errorf("seal failed before write: %w", err)
}

Try / catch

if _, meta, err := envelope.Seal(plain); err != nil {
	if strings.Contains(err.Error(), "marshal metadata") {
		// internal invariant issue: inspect Metadata struct changes
	}
	return err
}

Prevention

When it happens

Trigger: Calling Seal or SealForScope with a scheme/scope whose scopeHash or wrapped-key string somehow yields an unmarshalable Metadata value; in practice only a custom json.Marshaler injected via a modified Metadata type or a corrupted build.

Common situations: Barely seen in production; typically only during code changes where Metadata gains a field of a non-marshalable type (chan, func, cyclic pointer) or a MarshalJSON method that errors.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/db6242d5ef0e408f. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/envelope/envelope.go:95

	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, nil, fmt.Errorf("envelope: gcm: %w", err)
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, nil, fmt.Errorf("envelope: nonce entropy: %w", err)
	}
	ciphertext = gcm.Seal(nonce, nonce, plaintext, aad)

	wrapped, err := secretbox.EncryptPayloadKey(dataKey)
	if err != nil {
		return nil, nil, fmt.Errorf("envelope: wrap data key: %w", err)
	}
	meta := Metadata{Scheme: scheme, WrappedDataKey: base64.StdEncoding.EncodeToString(wrapped), ScopeHash: scopeHash}
	metaJSON, err = json.Marshal(meta)
	if err != nil {
		return nil, nil, fmt.Errorf("envelope: marshal metadata: %w", err)
	}
	return ciphertext, metaJSON, nil
}

// Open reverses Seal: it unwraps the data key from metadata and decrypts the
// ciphertext. An unknown scheme fails closed.
func Open(ciphertext []byte, metaJSON []byte) ([]byte, error) {
	var meta Metadata
	if err := json.Unmarshal(metaJSON, &meta); err != nil {
		return nil, fmt.Errorf("envelope: parse metadata: %w", err)
	}
	if meta.Scheme == schemeV2 {
		return nil, fmt.Errorf("envelope: tenant scope required for scheme %q", meta.Scheme)
	}
	if meta.Scheme != schemeV1 {
		return nil, fmt.Errorf("envelope: unknown scheme %q", meta.Scheme)
	}
	return open(ciphertext, meta, nil)

View on GitHub (pinned to 3ee70a1026)