chenhg5/cc-connect · error
wecom-ws: ciphertext not multiple of block size
Error message
wecom-ws: ciphertext not multiple of block size
What it means
AES-256-CBC decryption requires the ciphertext length to be an exact multiple of the 16-byte block size. wecomDecryptFile checks this after constructing the cipher and rejects any other length, since CBC cannot process a partial final block. This typically means the downloaded data is not actually the encrypted payload.
Source
Thrown at platform/wecom/websocket_media.go:286
// wecomDecryptFile decrypts payload from WeCom WS media URLs (AES-256-CBC, IV = first 16 key bytes).
// Same algorithm as @wecom/aibot-node-sdk decryptFile.
func wecomDecryptFile(ciphertext []byte, aesKeyB64 string) ([]byte, error) {
if len(ciphertext) == 0 {
return nil, fmt.Errorf("wecom-ws: empty ciphertext")
}
key, err := decodeWeComAESKey(aesKeyB64)
if err != nil {
return nil, err
}
key32 := key[:32]
iv := key32[:16]
block, err := aes.NewCipher(key32)
if err != nil {
return nil, err
}
if len(ciphertext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("wecom-ws: ciphertext not multiple of block size")
}
plain := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, ciphertext)
return pkcs7UnpadWeCom(plain)
}
func pkcs7UnpadWeCom(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("wecom-ws: empty padded data")
}
padLen := int(data[len(data)-1])
if padLen < 1 || padLen > 32 || padLen > len(data) {
return nil, fmt.Errorf("wecom-ws: invalid pkcs7 pad length %d", padLen)
}
for i := len(data) - padLen; i < len(data); i++ {
if int(data[i]) != padLen {
return nil, fmt.Errorf("wecom-ws: invalid pkcs7 padding")
}View on GitHub (pinned to 4000b2338a)
Solutions
- Log the first bytes and Content-Type of the downloaded body; if it's JSON/HTML, the URL is wrong or expired — fetch a fresh media URL.
- Only call wecomDecryptFile when the URL/flag indicates WS-encrypted media; otherwise return raw bytes.
- Check Content-Length against bytes read to detect truncation before decrypting.
Example fix
// before
raw, _ := io.ReadAll(lim)
return wecomDecryptFile(raw, aesKey)
// after
raw, _ := io.ReadAll(lim)
if len(raw)%16 != 0 {
return nil, fmt.Errorf("got %d bytes, ct=%q — media URL likely expired or not encrypted", len(raw), raw[:min(32, len(raw))])
}
return wecomDecryptFile(raw, aesKey) Defensive patterns
Strategy: validation
Validate before calling
if len(raw)%aes.BlockSize != 0 {
return fmt.Errorf("not ciphertext: %d bytes, head=%q", len(raw), raw[:min(16, len(raw))])
} Try / catch
plain, err := wecomDecryptFile(raw, key)
if err != nil && strings.Contains(err.Error(), "multiple of block size") {
log.Printf("body not encrypted (head=%q) — refresh media URL", raw[:min(32, len(raw))])
} Prevention
- Verify Content-Type is a binary/octet type before decrypting.
- Compare bytes read vs Content-Length to catch truncation.
- Only decrypt URLs known to be WS-encrypted media.
When it happens
Trigger: downloadWeComWSMedia downloads a body whose length % 16 != 0 (e.g. a JSON error body, an HTML login page, a truncated transfer) and then calls wecomDecryptFile on it.
Common situations: WeCom media URL expired and returned a small error JSON instead of the binary; HTTP transfer truncated by a proxy/timeout; applying decryption to a URL that was never encrypted (plain media endpoint).
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
- wecom-ws: invalid aeskey base64 length
- wecom-ws: decode aeskey: %w
- wecom-ws: aeskey decoded length %d, need >= 32
- wecom-ws: empty ciphertext
- wecom-ws: empty padded data
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/c9c610080135f56c.
Report an issue: GitHub.