sipeed/picoclaw · error
decrypt media: %w
Error message
decrypt media: %w
What it means
Optional AES-256-CBC decryption of inbound WeCom media failed (media.go:305-313). The key comes from the message payload (GetAESKey, base64 -> 32 bytes; IV = first 16 bytes of the key; PKCS7 unpad). The %w wraps concrete causes from decryptAESCBC: empty ciphertext, ciphertext length not a multiple of the 16-byte block, or invalid PKCS7 padding - the classic signatures of a key mismatch or of feeding plaintext to the decrypter.
Source
Thrown at pkg/channels/wecom/media.go:312
return "", fmt.Errorf("download media returned HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, wecomOutboundMediaMaxBytes+1))
if err != nil {
return "", fmt.Errorf("read media: %w", err)
}
if len(data) > wecomOutboundMediaMaxBytes {
return "", fmt.Errorf("media too large")
}
if aesKey != "" {
key, keyErr := decodeMediaAESKey(aesKey)
if keyErr != nil {
return "", keyErr
}
data, err = decryptAESCBC(key, data)
if err != nil {
return "", fmt.Errorf("decrypt media: %w", err)
}
}
filename, contentType := detectWeComMediaMetadata(
data,
msgID+fallbackExt,
resp.Header.Get("Content-Type"),
resourceURL,
resp.Header.Get("Content-Disposition"),
)
ext := filepath.Ext(filename)
if ext == "" {
ext = inferMediaExt(contentType, fallbackExt)
}
mediaDir := filepath.Join(os.TempDir(), "picoclaw_media")
if mkdirErr := os.MkdirAll(mediaDir, 0o700); mkdirErr != nil {
return "", fmt.Errorf("mkdir media dir: %w", mkdirErr)
}View on GitHub (pinned to 49183d7e8d)
Solutions
- Confirm the aes_key being used is the one from the same inbound message payload (payload.GetAESKey()), not a static config key
- Check len(body) % 16 == 0 before decrypting; if not, the body is almost certainly plaintext - skip decryption or treat as protocol error
- Verify the key decodes to exactly 32 bytes: WeCom EncodingAESKey is a 43-char base64 string (decodeMediaAESKey appends '=' if needed)
- Capture one failing URL+aes_key pair and reproduce decryption offline against the raw bytes to see the specific wrapped error (padding vs block size)
Example fix
// before: decrypt whenever a key is present
if aesKey != "" {
key, _ := decodeMediaAESKey(aesKey)
data, err = decryptAESCBC(key, data)
if err != nil {
return "", fmt.Errorf("decrypt media: %w", err)
}
}
// after: only attempt CBC when the body is block-aligned
if aesKey != "" && len(data)%aes.BlockSize == 0 && detectWeComFiletype(data) == ("", "") {
key, keyErr := decodeMediaAESKey(aesKey)
if keyErr != nil {
return "", keyErr
}
data, err = decryptAESCBC(key, data)
if err != nil {
return "", fmt.Errorf("decrypt media: %w", err)
}
} Defensive patterns
Strategy: validation
Validate before calling
// prechecks that eliminate the common decrypt failures
func mediaDecryptable(aesKey string, body []byte) bool {
if aesKey == "" {
return true // nothing to decrypt
}
key, err := base64.StdEncoding.DecodeString(aesKey + "=")
if err != nil || len(key) != 32 {
return false // malformed key
}
return len(body) > 0 && len(body)%16 == 0 // block-aligned ciphertext
} Prevention
- always pair the url and aes_key from the same message payload - never mix sources
- rotate keys on the WeCom admin console and redeploy together; stale keys decrypt nothing
- log len(body) and len(body)%16 on decrypt failure - non-zero remainder means plaintext/truncation, not a bad key
When it happens
Trigger: decodeMediaAESKey succeeded but decryptAESCBC rejected the body: aes_key from the payload does not match the corp/app that encrypted the media; the CDN body was actually unencrypted (often non-block-aligned, e.g. an HTML error page or a plain JPEG) while aesKey was non-empty; or the download was truncated to a non-multiple of 16.
Common situations: EncodingAESKey rotated on the WeCom admin side while messages still reference the old key; test/prod apps mixed up; CDN returning a 200 error page that then fails padding checks; key copied with trailing characters so it decodes to a wrong 32 bytes.
Related errors
- credential: enc:// decryption failed (wrong passphrase or SS
- media store not configured
- no media store available: %w
- create crypto helper: %w
- no media store available: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/60c0aef4b34e400a.
Report an issue: GitHub.