Tencent/WeKnora · error

ciphertext length is not a multiple of AES block size

Error message

ciphertext length is not a multiple of AES block size

What it means

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.

Source

Thrown at internal/im/wecom/webhook_adapter.go:463

}

// decrypt decrypts a WeCom AES-encrypted message.
func (a *WebhookAdapter) decrypt(encrypted string) ([]byte, error) {
	ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
	if err != nil {
		return nil, fmt.Errorf("base64 decode: %w", err)
	}

	block, err := aes.NewCipher(a.aesKey)
	if err != nil {
		return nil, fmt.Errorf("new cipher: %w", err)
	}

	if len(ciphertext) < aes.BlockSize {
		return nil, fmt.Errorf("ciphertext too short")
	}
	if len(ciphertext)%aes.BlockSize != 0 {
		return nil, fmt.Errorf("ciphertext length is not a multiple of AES block size")
	}

	iv := a.aesKey[:aes.BlockSize]
	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(ciphertext, ciphertext)

	// Remove and verify PKCS#7 padding
	padLen := int(ciphertext[len(ciphertext)-1])
	if padLen > wecomPKCS7BlockSize || padLen == 0 || padLen > len(ciphertext) {
		return nil, fmt.Errorf("invalid padding")
	}
	for i := 0; i < padLen; i++ {
		if ciphertext[len(ciphertext)-1-i] != byte(padLen) {
			return nil, fmt.Errorf("invalid padding")
		}
	}
	plaintext := ciphertext[:len(ciphertext)-padLen]

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Check len(ciphertext)%16 after base64 decode; if not 0, the payload was corrupted in transit — log and request a fresh callback
  2. Do not trim, URL-decode twice, or whitespace-strip the base64 string beyond standard decoding
  3. Confirm you are not decrypting already-decrypted/plaintext content
  4. Re-run the WeCom callback verification flow to obtain a fresh valid echostr
Defensive patterns

Strategy: validation

Validate before calling

raw, err := base64.StdEncoding.DecodeString(encryptParam)
if err != nil || len(raw)%16 != 0 { /* reject before calling ParseCallback */ }

Type guard

func isBlockAligned(b []byte) bool { return len(b) > 0 && len(b)%16 == 0 }

Try / catch

msg, err := adapter.ParseCallback(sig, ts, nonce, enc)
if err != nil && strings.Contains(err.Error(), "multiple of AES block size") {
    return fmt.Errorf("corrupted callback payload: %w", err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/af045249dae333b9. Report an issue: GitHub.