AES-CBC is a mode of operation that encrypts AES blocks in a chain, where each 16-byte plaintext block is XORed with the previous ciphertext block before encryption. CBC stands for cipher block chaining and is defined in NIST SP 800-38A. The first block uses a 16-byte initialization vector (IV). CBC gives confidentiality only, so tampering is not detected, and TLS 1.3 keeps only authenticated ciphers.
For block i, the encryptor computes C(i) = AES(P(i) XOR C(i-1)), with C(0) set to the IV. Decryption computes P(i) = AES-decrypt(C(i)) XOR C(i-1). Because of the chaining, equal plaintext blocks produce different ciphertext blocks, which fixes the main weakness of ECB.
The details developers must handle:
The example encrypts "hello" with Python and OpenSSL. Both produced the same 16 bytes.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
key = bytes(range(16)); iv = bytes(range(16, 32))
p = padding.PKCS7(128).padder()
d = p.update(b"hello") + p.finalize()
print("padded", d.hex())
e = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
c = e.update(d) + e.finalize()
print("cbc", len(c), c.hex())
padded 68656c6c6f0b0b0b0b0b0b0b0b0b0b0b
cbc 16 32f6d6ea1ec8872debccea8596d9c3c3
AES-CBC output is the input length rounded up to the next multiple of 16 bytes, plus one more block when the input is already a multiple of 16. Padded lengths measured from PKCS#7: 0, 1, 15 bytes give 16; 16, 17 and 31 give 32; 32 gives 48. Store the 16-byte IV as well, so a 5-byte message takes 32 bytes in total.
AES-CBC is secure for confidentiality when the IV is unpredictable and the key is strong, but it is not authenticated. Anyone who can change the data can flip bits. Changing one byte of the IV turned the decrypted first block amount=0000100.0 into amount=9000100.0, while the ciphertext still decrypted without any error. Add an HMAC over the IV and ciphertext, check it before decrypting, or use AES-GCM.