{"record":{"id":"c9c610080135f56c","repo":"chenhg5/cc-connect","slug":"wecom-ws-ciphertext-not-multiple-of-block-size","errorCode":null,"errorMessage":"wecom-ws: ciphertext not multiple of block size","messagePattern":"wecom-ws: ciphertext not multiple of block size","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/websocket_media.go","lineNumber":286,"sourceCode":"// wecomDecryptFile decrypts payload from WeCom WS media URLs (AES-256-CBC, IV = first 16 key bytes).\n// Same algorithm as @wecom/aibot-node-sdk decryptFile.\nfunc wecomDecryptFile(ciphertext []byte, aesKeyB64 string) ([]byte, error) {\n\tif len(ciphertext) == 0 {\n\t\treturn nil, fmt.Errorf(\"wecom-ws: empty ciphertext\")\n\t}\n\tkey, err := decodeWeComAESKey(aesKeyB64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkey32 := key[:32]\n\tiv := key32[:16]\n\n\tblock, err := aes.NewCipher(key32)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(ciphertext)%aes.BlockSize != 0 {\n\t\treturn nil, fmt.Errorf(\"wecom-ws: ciphertext not multiple of block size\")\n\t}\n\tplain := make([]byte, len(ciphertext))\n\tcipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, ciphertext)\n\treturn pkcs7UnpadWeCom(plain)\n}\n\nfunc pkcs7UnpadWeCom(data []byte) ([]byte, error) {\n\tif len(data) == 0 {\n\t\treturn nil, fmt.Errorf(\"wecom-ws: empty padded data\")\n\t}\n\tpadLen := int(data[len(data)-1])\n\tif padLen < 1 || padLen > 32 || padLen > len(data) {\n\t\treturn nil, fmt.Errorf(\"wecom-ws: invalid pkcs7 pad length %d\", padLen)\n\t}\n\tfor i := len(data) - padLen; i < len(data); i++ {\n\t\tif int(data[i]) != padLen {\n\t\t\treturn nil, fmt.Errorf(\"wecom-ws: invalid pkcs7 padding\")\n\t\t}","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket_media.go#L268-L304","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nraw, _ := io.ReadAll(lim)\nreturn wecomDecryptFile(raw, aesKey)\n// after\nraw, _ := io.ReadAll(lim)\nif len(raw)%16 != 0 {\n    return nil, fmt.Errorf(\"got %d bytes, ct=%q — media URL likely expired or not encrypted\", len(raw), raw[:min(32, len(raw))])\n}\nreturn wecomDecryptFile(raw, aesKey)","handlingStrategy":"validation","validationCode":"if len(raw)%aes.BlockSize != 0 {\n    return fmt.Errorf(\"not ciphertext: %d bytes, head=%q\", len(raw), raw[:min(16, len(raw))])\n}","typeGuard":null,"tryCatchPattern":"plain, err := wecomDecryptFile(raw, key)\nif err != nil && strings.Contains(err.Error(), \"multiple of block size\") {\n    log.Printf(\"body not encrypted (head=%q) — refresh media URL\", raw[:min(32, len(raw))])\n}","preventionTips":["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."],"tags":["wecom","crypto","aes-cbc","block-size"],"backgroundTag":"invalid-argument-value","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}