{"record":{"id":"60c0aef4b34e400a","repo":"sipeed/picoclaw","slug":"decrypt-media-w","errorCode":null,"errorMessage":"decrypt media: %w","messagePattern":"decrypt media: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/wecom/media.go","lineNumber":312,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"download media returned HTTP %d\", resp.StatusCode)\n\t}\n\n\tdata, err := io.ReadAll(io.LimitReader(resp.Body, wecomOutboundMediaMaxBytes+1))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"read media: %w\", err)\n\t}\n\tif len(data) > wecomOutboundMediaMaxBytes {\n\t\treturn \"\", fmt.Errorf(\"media too large\")\n\t}\n\n\tif aesKey != \"\" {\n\t\tkey, keyErr := decodeMediaAESKey(aesKey)\n\t\tif keyErr != nil {\n\t\t\treturn \"\", keyErr\n\t\t}\n\t\tdata, err = decryptAESCBC(key, data)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"decrypt media: %w\", err)\n\t\t}\n\t}\n\n\tfilename, contentType := detectWeComMediaMetadata(\n\t\tdata,\n\t\tmsgID+fallbackExt,\n\t\tresp.Header.Get(\"Content-Type\"),\n\t\tresourceURL,\n\t\tresp.Header.Get(\"Content-Disposition\"),\n\t)\n\text := filepath.Ext(filename)\n\tif ext == \"\" {\n\t\text = inferMediaExt(contentType, fallbackExt)\n\t}\n\tmediaDir := filepath.Join(os.TempDir(), \"picoclaw_media\")\n\tif mkdirErr := os.MkdirAll(mediaDir, 0o700); mkdirErr != nil {\n\t\treturn \"\", fmt.Errorf(\"mkdir media dir: %w\", mkdirErr)\n\t}","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/wecom/media.go#L294-L330","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before: decrypt whenever a key is present\nif aesKey != \"\" {\n    key, _ := decodeMediaAESKey(aesKey)\n    data, err = decryptAESCBC(key, data)\n    if err != nil {\n        return \"\", fmt.Errorf(\"decrypt media: %w\", err)\n    }\n}\n\n// after: only attempt CBC when the body is block-aligned\nif aesKey != \"\" && len(data)%aes.BlockSize == 0 && detectWeComFiletype(data) == (\"\", \"\") {\n    key, keyErr := decodeMediaAESKey(aesKey)\n    if keyErr != nil {\n        return \"\", keyErr\n    }\n    data, err = decryptAESCBC(key, data)\n    if err != nil {\n        return \"\", fmt.Errorf(\"decrypt media: %w\", err)\n    }\n}","handlingStrategy":"validation","validationCode":"// prechecks that eliminate the common decrypt failures\nfunc mediaDecryptable(aesKey string, body []byte) bool {\n    if aesKey == \"\" {\n        return true // nothing to decrypt\n    }\n    key, err := base64.StdEncoding.DecodeString(aesKey + \"=\")\n    if err != nil || len(key) != 32 {\n        return false // malformed key\n    }\n    return len(body) > 0 && len(body)%16 == 0 // block-aligned ciphertext\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["crypto","aes","wecom","media","configuration"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}