JuliusBrussee/caveman · error

aes-gcm open

Error message

aes-gcm open: %w

What it means

AES-GCM Open failed to authenticate/decrypt the envelope: the nonce was extracted but the ciphertext did not verify under the configured key. GCM is an AEAD, so any key mismatch, tampering, or corruption causes Open to return an error, which secretbox wraps as "aes-gcm open: ...". This is the standard failure when the wrong key is configured or the ciphertext was modified.

Solutions

  1. Confirm the decryption-side key (e.g. SECRETBOX_KEY env) is byte-identical to the key used to encrypt; re-export it and retry.
  2. Re-encrypt the secret with the current key and replace the stored ciphertext instead of trying to fix it.
  3. Check whether the ciphertext was modified in transit/storage (compare checksums or re-copy the base64 blob verbatim).
  4. If keys were rotated, decrypt with the old key and re-encrypt with the new one; keep old keys available during rotation windows.

Example fix

// before (mismatched key picked up from shell profile)
export SECRETBOX_KEY="stale-key"
plain, err := secretbox.ResolveEnvironmentSecret("", "APP_SECRET") // aes-gcm open: cipher: message authentication failed

// after (fail fast on key mismatch)
if os.Getenv("SECRETBOX_KEY") != expectedKeyFingerprint {
    return fmt.Errorf("SECRETBOX_KEY does not match the key this secret was encrypted with")
}
plain, err := secretbox.ResolveEnvironmentSecret("", "APP_SECRET")
Defensive patterns

Strategy: try-catch

Validate before calling

// compare key fingerprints without revealing the key
if sha256.Sum256([]byte(os.Getenv("SECRETBOX_KEY"))) != expectedKeySum {
    return fmt.Errorf("SECRETBOX_KEY fingerprint mismatch for this environment")
}

Try / catch

plain, err := secretbox.Decrypt(envelope)
if err != nil {
    var authFail = strings.Contains(err.Error(), "aes-gcm open")
    if authFail {
        return fmt.Errorf("ciphertext cannot be decrypted with the configured key; check rotation: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Decrypt (or ResolveEnvironmentSecret after base64 decode) receives a well-formed, correctly sized envelope whose ciphertext, nonce, or key differs from the one used at encryption time — wrong key in the environment, edited ciphertext, or bytes re-encoded lossily (e.g. latin-1 instead of base64).

Common situations: Rotating SECRETBOX_KEY in one environment but not the one decrypting; the encrypted value was hand-edited or copy-pasted with a dropped character; two services share an env var name but different keys; a config store trimmed/normalized the value.

Related errors


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

Appendix: source

Thrown at shared/platform/secretbox/secretbox.go:137

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

// DecryptPayloadKey unwraps an artifact data-encryption key. KMS envelopes are
// restricted to the configured payload key plus the explicit legacy secrets
// key used before key separation.
func DecryptPayloadKey(envelope []byte) ([]byte, error) {
	if kms.IsEnvelope(envelope) {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		plaintext, err := kms.DecryptPayload(ctx, envelope)
		if err != nil {
			return nil, fmt.Errorf("secretbox: payload KMS decrypt: %w", err)
		}
		return plaintext, nil
	}
	return Decrypt(envelope)

View on GitHub (pinned to 3ee70a1026)