JuliusBrussee/caveman · error
aes cipher
Error message
aes cipher: %w
What it means
After loading the local key, secretbox.Encrypt calls aes.NewCipher(keyBytes) to build an AES block cipher. Go's aes package returns an error only when the key length is not 16, 24, or 32 bytes (AES-128/192/256). This error therefore means the key material produced by loadKey() has an invalid length.
Solutions
- Print len(keyBytes) at loadKey (or decode manually in a scratch program) and confirm it is exactly 16, 24, or 32 bytes.
- Regenerate the key correctly, e.g. openssl rand -base64 32, and set the env var with no quoting/newline issues.
- Ensure the decoding scheme in loadKey (hex vs base64) matches how the key was generated and stored.
- Trim whitespace/newlines before decoding, or fix the stored value rather than trimming at runtime.
Example fix
// before: raw passphrase used as key -> aes cipher: crypto/aes: invalid key size 11
key := os.Getenv("CAVE_SECRETBOX_KEY")
// after: exactly 32 random bytes, base64-decoded
keyB64 := strings.TrimSpace(os.Getenv("CAVE_SECRETBOX_KEY"))
key, err := base64.StdEncoding.DecodeString(keyB64) // len == 32
if err != nil || (len(key) != 16 && len(key) != 24 && len(key) != 32) {
log.Fatal("CAVE_SECRETBOX_KEY must decode to 16/24/32 bytes")
} Defensive patterns
Strategy: validation
Validate before calling
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(os.Getenv("CAVE_SECRETBOX_KEY")))
if err != nil || (len(key) != 16 && len(key) != 24 && len(key) != 32) {
return fmt.Errorf("CAVE_SECRETBOX_KEY must decode to 16/24/32 bytes, got %d", len(key))
} Try / catch
cipherText, err := secretbox.Encrypt(pt)
if err != nil {
if strings.Contains(err.Error(), "aes cipher") {
return fmt.Errorf("misconfigured secretbox key: %w", err)
}
return err
} Prevention
- Generate keys with a fixed recipe (openssl rand -base64 32) and store them without added quoting or newlines.
- Validate key length at startup, not at first use.
- Never pass a human-chosen passphrase where a byte-exact AES key is required.
- Keep generation and decoding encoding (hex vs base64) documented alongside the key.
When it happens
Trigger: Calling secretbox.Encrypt with useKMS() false (non-production) when CAVE_SECRETBOX_KEY (or whatever loadKey reads) decodes to a byte slice whose length is not 16/24/32 — e.g. a hex/base64 string truncated, double-encoded, or with stray whitespace/newline included in the decoded bytes.
Common situations: Pasting a key with a trailing newline or quotes into the env var; using a raw passphrase string of arbitrary length instead of a decoded 32-byte key; key file truncated by copy/paste or editor; switching between hex and base64 encodings without adjusting.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- must decode to exactly 32 bytes, got
- secretbox: payload KMS encrypt
- secretbox: production requires CAVE_KMS_PROVIDER=scaleway
- aes-gcm
- budget is below CCR storage minimum
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/172b346b29723217.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/secretbox/secretbox.go:72
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
}
if runtimeenv.IsProduction() {
return nil, fmt.Errorf("secretbox: production requires CAVE_KMS_PROVIDER=scaleway")
}
keyBytes, err := loadKey()
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)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("nonce entropy: %w", err)
}
// Seal appends the ciphertext+tag to nonce, so the returned slice is the
// full nonce||ciphertext envelope.
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
// EncryptPayloadKey wraps an artifact data-encryption key. Production uses the
// dedicated payload KEK; local development retains the same AES-GCM envelope as
// other local secrets.
func EncryptPayloadKey(plaintext []byte) ([]byte, error) {View on GitHub (pinned to 3ee70a1026)