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

  1. Validate len(ciphertext)%16 == 0 before the call and surface which encoding step broke alignment.
  2. Strip any IV/header prefix before passing data to CBCDecryption (note AESEncrypt output is IV+ciphertext and meant for AESDecrypt, not this API).
  3. Re-encode the payload (hex/base64) and compare lengths to find where bytes were lost.
  4. 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

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


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)