OpenNHP/opennhp · error
ciphertext length is not a multiple of block size
Error message
ciphertext length %d is not a multiple of block size %d
What it means
CBC is a block mode: ciphertext length must be an exact multiple of the block size (16 bytes for AES/SM4). CBCDecryption checks this before decrypting because cipher.NewCBCDecrypter would panic on partial blocks; this error converts that into a clean failure.
Solutions
- Validate len(ciphertext)%16 == 0 before the call and surface which encoding step broke alignment.
- Strip any IV/header prefix before passing data to CBCDecryption (note AESEncrypt output is IV+ciphertext and meant for AESDecrypt, not this API).
- Re-encode the payload (hex/base64) and compare lengths to find where bytes were lost.
- Enable integrity protection (AEAD) on the transport if corruption in transit is recurring.
Example fix
// before
plain, err := core.CBCDecryption(core.GCM_AES256, key, ivAndCt, false) // includes 16-byte IV
// after
if len(ivAndCt) < 16 || (len(ivAndCt)-16)%16 != 0 {
return fmt.Errorf("bad ciphertext framing")
}
plain, err := core.CBCDecryption(core.GCM_AES256, key, ivAndCt[16:], false) Defensive patterns
Strategy: validation
Validate before calling
if len(ct)%16 != 0 {
return fmt.Errorf("misaligned ciphertext: %d bytes (IV stripped?)", len(ct))
} Try / catch
plain, err := core.CBCDecryption(t, key, ct, false)
if err != nil {
return fmt.Errorf("CBC decrypt (len=%d): %w", len(ct), err)
} Prevention
- Strip IV prefixes before CBCDecryption; AESEncrypt output belongs to AESDecrypt, not this API.
- Use base64/hex round-trips carefully; compare lengths after encode/decode.
- Prefer AEAD modes which detect corruption instead of misaligning.
When it happens
Trigger: Calling CBCDecryption with ciphertext whose length isn't a multiple of 16 — a byte dropped or added in transit, wrong slicing (headers/IV not removed), or base64/hex decoding that lost characters.
Common situations: Manual wire-format parsing that mis-slices the payload; corruption over an unprotected transport; mixing ciphertext formats (e.g. IV-prefixed AESEncrypt output fed to CBCDecryption without stripping the IV); copy-paste truncating trailing bytes.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- ciphertext too short: need at least
- unsupported cipher type for CBC decryption
- cipherText too short: need at least
- cipherText length invalid: must be IV + multiple of block…
- failed to create device
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/3153e8d6d381a9a9.
Report an issue: GitHub.
Appendix: source
Thrown at nhp/core/crypto.go:267
block, err = sm4.NewCipher(key[:16])
if err != nil {
return nil, fmt.Errorf("failed to create SM4 cipher for CBC decryption: %w", err)
}
iv = key[16:]
case GCM_CHACHA20POLY1305:
return nil, ErrNotApplicable
default:
return nil, fmt.Errorf("unsupported cipher type for CBC decryption: %d", t)
}
// Validate ciphertext: must be at least one block and a multiple of block size
if len(ciphertext) < block.BlockSize() {
return nil, fmt.Errorf("ciphertext too short: need at least %d bytes", block.BlockSize())
}
if len(ciphertext)%block.BlockSize() != 0 {
return nil, fmt.Errorf("ciphertext length %d is not a multiple of block size %d", len(ciphertext), block.BlockSize())
}
var plaintext []byte
if inPlace {
plaintext = ciphertext
} else {
plaintext = make([]byte, len(ciphertext))
}
mode := cipher.NewCBCDecrypter(block, iv)
// CryptBlocks can work in-place if the two arguments are the same.
mode.CryptBlocks(plaintext, ciphertext)
if len(plaintext)%block.BlockSize() == 0 {
// skip unpadding
} else {
// Unpad plaintext
pkcs7 := padding.NewPKCS7Padding(uint(block.BlockSize()))View on GitHub (pinned to 6e04ca5ff0)