OpenNHP/opennhp · error

cipherText length invalid: must be IV + multiple of block…

Error message

cipherText length invalid: must be IV + multiple of block size

What it means

After reserving the 16-byte IV, the remaining ciphertext in AESDecrypt must be an exact multiple of the 16-byte AES block size, since CBC decryption cannot process partial blocks (Go's CBC decrypter would panic). This guard rejects misaligned input with a clean error.

Solutions

  1. Validate (len(data)-16)%16 == 0 before calling; trace which serialization step changed the byte count.
  2. Store/transfer ciphertext as raw bytes or length-safe encodings (base64) and decode before decrypting.
  3. Only feed AESDecrypt data produced by AESEncrypt (IV + CBC blocks); use matching APIs for other modes (e.g. AeadFromKey for GCM).
  4. Verify no header/footer bytes are included in the slice passed in.

Example fix

// before
raw := []byte(b64Stored) // base64 text decrypted directly
plain, err := core.AESDecrypt(raw, key)
// after
data, err := base64.StdEncoding.DecodeString(b64Stored)
if (len(data)-16)%16 != 0 {
    return nil, fmt.Errorf("corrupt blob: %d bytes", len(data))
}
plain, err := core.AESDecrypt(data, key)
Defensive patterns

Strategy: validation

Validate before calling

if (len(blob)-16)%16 != 0 {
    return fmt.Errorf("blob not IV+block-aligned: %d bytes", len(blob))
}

Try / catch

plain, err := core.AESDecrypt(blob, key)
if err != nil {
    return fmt.Errorf("AES decrypt framing invalid (len=%d): %w", len(blob), err)
}

Prevention

When it happens

Trigger: Calling AESDecrypt where len(cipherText)-16 is not a multiple of 16 — one or more bytes were added or lost after the IV, or the input isn't AESEncrypt output at all (e.g. raw CBC ciphertext without an IV, or base64 text passed as bytes).

Common situations: Storing ciphertext as text and losing/altering bytes in encoding round-trips; concatenating multiple encrypted blobs; passing GCM or other-mode output to this CBC helper; manual slicing that keeps the wrong offset.

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/0f5de0799104d174. Report an issue: GitHub.

Appendix: source

Thrown at nhp/core/crypto.go:375

// pad adds PKCS#7 padding to data. Uses shared implementation from utils.
func pad(data []byte, blockSize int) []byte {
	return utils.PKCS7Pad(data, blockSize)
}

func AESDecrypt(cipherText []byte, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	// Validate ciphertext length:
	// - Must have at least IV (16 bytes) + one encrypted block (16 bytes)
	// - After IV extraction, remaining must be a multiple of block size
	if len(cipherText) < aes.BlockSize*2 {
		return nil, fmt.Errorf("cipherText too short: need at least %d bytes, got %d", aes.BlockSize*2, len(cipherText))
	}
	if (len(cipherText)-aes.BlockSize)%aes.BlockSize != 0 {
		return nil, fmt.Errorf("cipherText length invalid: must be IV + multiple of block size")
	}
	iv := cipherText[:aes.BlockSize]
	cipherText = cipherText[aes.BlockSize:]

	// Decrypt
	mode := cipher.NewCBCDecrypter(block, iv)
	decrypted := make([]byte, len(cipherText))
	mode.CryptBlocks(decrypted, cipherText)

	// Remove padding
	decrypted = unpad(decrypted, aes.BlockSize)

	return decrypted, nil
}

// unpad removes PKCS#7 padding from data. Uses shared implementation from utils.
func unpad(padded []byte, blockSize int) []byte {
	return utils.PKCS7Unpad(padded, blockSize)

View on GitHub (pinned to 6e04ca5ff0)