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, nilView on GitHub (pinned to 27d5a3981a)
Solutions
- Regenerate cleanly: openssl rand -base64 32 produces exactly the accepted form; re-export and confirm with a decode check.
- Strip whitespace/newlines/quotes from the value when loading it (or fix the env file so they were never stored).
- 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
- Generate with openssl rand -base64 32 only; avoid re-encoding or switching alphabets.
- Trim whitespace/newlines when reading env files; beware CRLF from Windows editors.
- Preflight the key (decodes + 32 bytes) at process start.
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
- %s is not set; cannot encrypt/decrypt secrets
- cave_vercel_terminal_failure
- CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM is required
- %s: %w
- %s must decode to exactly 32 bytes, got %d
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/bb8e046b8083c651.
Report an issue: GitHub.