JuliusBrussee/caveman · error

envelope: open

Error message

envelope: open: %w

What it means

GCM authenticated decryption failed in envelope.Open/OpenForScope: gcm.Open returned an error because the ciphertext, nonce, or additional authenticated data (AAD) does not match what was sealed. GCM is an authenticated mode, so any tampering, key mismatch, or scope mismatch surfaces as this wrapped error. The original plaintext is never exposed.

Solutions

  1. Confirm the same data key / key reference used at seal time is being used at open time (check key version, keyring, env)
  2. For OpenForScope, ensure the Scope (OrganizationID, ProjectID, Kind) exactly matches the one passed to SealForScope — AAD is bound via SHA-256
  3. Re-seal the data with the current key/scope if the original credentials are unrecoverable
  4. Check whether the stored ciphertext was altered (encoding round-trips, whitespace trimming, JSON escaping)

Example fix

// before: mismatched scope on open
pt, err := envelope.OpenForScope(ctx, key, ct, envelope.Scope{OrganizationID: orgID, Kind: "note"})
// after: reuse the exact scope recorded at seal time
pt, err := envelope.OpenForScope(ctx, key, ct, envelope.Scope{OrganizationID: orgID, ProjectID: projID, Kind: "document"})
Defensive patterns

Strategy: try-catch

Validate before calling

// cannot pre-validate integrity; ensure key ref and scope match what was used at seal time
// store the key version and scope hash alongside the ciphertext

Try / catch

pt, err := envelope.OpenForScope(ctx, key, ct, scope)
if err != nil && strings.Contains(err.Error(), "envelope: open:") {
    // wrong key or tampered/mismatched AAD: do not retry blindly;
    // check key rotation history and scope equality before failing
    return ErrDecryptionMismatch
}

Prevention

When it happens

Trigger: Calling envelope.Open with the wrong data key (different keyring/key version), or envelope.OpenForScope with a Scope whose OrganizationID/ProjectID/Kind differ from those used at SealForScope time, or with ciphertext that was modified or corrupted in storage.

Common situations: Rotating keys without re-encrypting old rows; changing the scope kind string (e.g. "doc" vs "document") between seal and open; a migration edited encrypted bytes; an env pointed at a different KMS key than the one that sealed the data.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

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

	if err != nil {
		return nil, fmt.Errorf("envelope: unwrap data key: %w", err)
	}
	block, err := aes.NewCipher(dataKey)
	if err != nil {
		return nil, fmt.Errorf("envelope: aes: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("envelope: gcm: %w", err)
	}
	ns := gcm.NonceSize()
	if len(ciphertext) < ns {
		return nil, fmt.Errorf("envelope: ciphertext too short")
	}
	nonce, ct := ciphertext[:ns], ciphertext[ns:]
	plaintext, err := gcm.Open(nil, nonce, ct, aad)
	if err != nil {
		return nil, fmt.Errorf("envelope: open: %w", err)
	}
	return plaintext, nil
}

func scopeAAD(scope Scope) ([]byte, string, error) {
	scope.OrganizationID = strings.TrimSpace(scope.OrganizationID)
	scope.ProjectID = strings.TrimSpace(scope.ProjectID)
	scope.Kind = strings.TrimSpace(scope.Kind)
	if scope.OrganizationID == "" {
		return nil, "", fmt.Errorf("envelope: organization scope is required")
	}
	if scope.Kind == "" {
		return nil, "", fmt.Errorf("envelope: object kind is required")
	}
	aad, err := json.Marshal(struct {
		Version        int    `json:"version"`
		OrganizationID string `json:"organization_id"`
		ProjectID      string `json:"project_id"`

View on GitHub (pinned to 3ee70a1026)