JuliusBrussee/caveman · error

%s is not valid base64: %w

Error message

%s is not valid base64: %w

What it means

CAVE_LOCAL_ENCRYPTION_KEY was set but base64.StdEncoding.DecodeString failed on it, so it cannot be raw key material. Std (padded, standard-alphabet) base64 is required: the error commonly means URL-safe characters ('-','_' from base64.URLEncoding), missing padding, stray whitespace/quotes from shell or dotenv handling, or the value was generated as hex or raw bytes instead of base64.

Source

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

	"time"

	"github.com/JuliusBrussee/caveman/shared/platform/kms"
	"github.com/JuliusBrussee/caveman/shared/platform/runtimeenv"
)

// envKey is the name of the environment variable holding the base64-encoded
// 32-byte master key.
const envKey = "CAVE_LOCAL_ENCRYPTION_KEY"

// loadKey reads and validates the 32-byte AES key from the environment.
func loadKey() ([]byte, error) {
	keyB64 := os.Getenv(envKey)
	if keyB64 == "" {
		return nil, fmt.Errorf("%s is not set; cannot encrypt/decrypt secrets", envKey)
	}
	keyBytes, err := base64.StdEncoding.DecodeString(keyB64)
	if err != nil {
		return nil, fmt.Errorf("%s is not valid base64: %w", envKey, err)
	}
	if len(keyBytes) != 32 {
		return nil, fmt.Errorf("%s must decode to exactly 32 bytes, got %d", envKey, len(keyBytes))
	}
	return keyBytes, nil
}

// Encrypt seals plaintext with AES-256-GCM and a fresh random nonce, returning
// nonce(12) || ciphertext+tag as raw bytes.
func Encrypt(plaintext []byte) ([]byte, error) {
	if useKMS() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		wrapped, err := kms.Encrypt(ctx, plaintext)
		if err != nil {
			return nil, fmt.Errorf("secretbox: KMS encrypt: %w", err)
		}
		return wrapped, nil

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Regenerate cleanly: openssl rand -base64 32 produces exactly the accepted form; re-export and confirm with a decode check.
  2. Strip whitespace/newlines/quotes from the value when loading it (or fix the env file so they were never stored).
  3. If the key must be URL-safe base64, transcode it first: base64.URLEncoding -> raw bytes -> base64.StdEncoding.

Example fix

# before (URL-safe alphabet or hex -> DecodeString fails)
export CAVE_LOCAL_ENCRYPTION_KEY='c2hvcnR'   # wrong length/alphabet

# after
export CAVE_LOCAL_ENCRYPTION_KEY="$(openssl rand -base64 32)" # 44 chars, standard alphabet, padded
Defensive patterns

Strategy: validation

Validate before calling

func localKeyDecodable() bool {
    v := os.Getenv("CAVE_LOCAL_ENCRYPTION_KEY")
    if v == "" { return false }
    _, err := base64.StdEncoding.DecodeString(strings.TrimSpace(v))
    return err == nil
}

Try / catch

if _, err := secretbox.Encrypt(pt); err != nil {
    if strings.Contains(err.Error(), "not valid base64") {
        // regenerate as: openssl rand -base64 32; check for stray quotes/whitespace/CRLF in the env file
    }
}

Prevention

When it happens

Trigger: Encrypt/Decrypt in local mode where the env value contains characters outside the standard alphabet, has wrong/missing '=' padding, or wraps a quote/newline — e.g. KEY="abc..." where the quotes became part of the value, or a 64-char hex string.

Common situations: Copying keys between systems that use different base64 variants; YAML/env files that fold long lines or append \r (CRLF); pasting with trailing newline from a password manager; hex output from xxd used where base64 was expected.

Related errors


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