{"record":{"id":"af045249dae333b9","repo":"Tencent/WeKnora","slug":"ciphertext-length-is-not-a-multiple-of-aes-block-s","errorCode":null,"errorMessage":"ciphertext length is not a multiple of AES block size","messagePattern":"ciphertext length is not a multiple of AES block size","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/im/wecom/webhook_adapter.go","lineNumber":463,"sourceCode":"}\n\n// decrypt decrypts a WeCom AES-encrypted message.\nfunc (a *WebhookAdapter) decrypt(encrypted string) ([]byte, error) {\n\tciphertext, err := base64.StdEncoding.DecodeString(encrypted)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"base64 decode: %w\", err)\n\t}\n\n\tblock, err := aes.NewCipher(a.aesKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new cipher: %w\", err)\n\t}\n\n\tif len(ciphertext) < aes.BlockSize {\n\t\treturn nil, fmt.Errorf(\"ciphertext too short\")\n\t}\n\tif len(ciphertext)%aes.BlockSize != 0 {\n\t\treturn nil, fmt.Errorf(\"ciphertext length is not a multiple of AES block size\")\n\t}\n\n\tiv := a.aesKey[:aes.BlockSize]\n\tmode := cipher.NewCBCDecrypter(block, iv)\n\tmode.CryptBlocks(ciphertext, ciphertext)\n\n\t// Remove and verify PKCS#7 padding\n\tpadLen := int(ciphertext[len(ciphertext)-1])\n\tif padLen > wecomPKCS7BlockSize || padLen == 0 || padLen > len(ciphertext) {\n\t\treturn nil, fmt.Errorf(\"invalid padding\")\n\t}\n\tfor i := 0; i < padLen; i++ {\n\t\tif ciphertext[len(ciphertext)-1-i] != byte(padLen) {\n\t\t\treturn nil, fmt.Errorf(\"invalid padding\")\n\t\t}\n\t}\n\tplaintext := ciphertext[:len(ciphertext)-padLen]\n","sourceCodeStart":445,"sourceCodeEnd":481,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/im/wecom/webhook_adapter.go#L445-L481","documentation":"decrypt() requires the ciphertext length to be an exact multiple of the 16-byte AES block size, as CBC mode demands. A length not aligned to the block size means the payload was corrupted, truncated, or was never AES-encrypted at all, so decryption is refused. There is a dedicated test (TestWebhookAdapterDecryptRejectsNonBlockAlignedCiphertext) asserting this behavior.","triggerScenarios":"Base64-decoded encrypted payload whose length % 16 != 0 — e.g. a test posting a non-block-aligned string, URL-encoding corruption dropping characters, or decrypting a plaintext string by mistake.","commonSituations":"Callback proxies re-encoding the Encrypt parameter; manual copy/paste of tokens losing trailing characters (base64 '=' padding); double-decoding the payload so length shrinks below block alignment.","solutions":["Check len(ciphertext)%16 after base64 decode; if not 0, the payload was corrupted in transit — log and request a fresh callback","Do not trim, URL-decode twice, or whitespace-strip the base64 string beyond standard decoding","Confirm you are not decrypting already-decrypted/plaintext content","Re-run the WeCom callback verification flow to obtain a fresh valid echostr"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"raw, err := base64.StdEncoding.DecodeString(encryptParam)\nif err != nil || len(raw)%16 != 0 { /* reject before calling ParseCallback */ }","typeGuard":"func isBlockAligned(b []byte) bool { return len(b) > 0 && len(b)%16 == 0 }","tryCatchPattern":"msg, err := adapter.ParseCallback(sig, ts, nonce, enc)\nif err != nil && strings.Contains(err.Error(), \"multiple of AES block size\") {\n    return fmt.Errorf(\"corrupted callback payload: %w\", err)\n}","preventionTips":["Never alter the base64 string between receipt and decrypt","Enable msg_signature verification so corrupted payloads are rejected earlier","Add a block-alignment unit test mirroring TestWebhookAdapterDecryptRejectsNonBlockAlignedCiphertext"],"tags":["aes","decryption","block-size","wecom"],"backgroundTag":"ciphertext-not-block-aligned","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}