Security is not additive, but subtractive—TLS 1.3 achieves this through a “minimalist” cipher suite, removing over 10 features (such as compression and renegotiation), and reducing the cipher suites from 37 to 5,AES-CBC was directly removed due to security risks.
Meanwhile, the ChaCha20-Poly1305 algorithm has become the new favorite in the TLS protocol, gradually taking center stage.
Undoubtedly, the removal of AES-CBC in TLS 1.3 isa watershed moment in cryptographic evolution: it marks the end of the era of “encryption without authentication” and promotes AEAD as the new standard.
A visual comparison of the differences between old and new cryptographic algorithms
Image Interpretation
- AES-CBC: operates like cutting tofu blocks (block encryption), where data is divided into fixed small blocks, each block encrypted but dependent on the encryption result of the previous block (“chain dependency”). If the last block is not full, padding is required (“padding”). The biggest issue is that it only handles encryption, not authentication, making it vulnerable to attacks (such as “chosen ciphertext attacks”).
- AES-GCM: is also part of the AES family, but it adopts a new approach using the “counter” (CTR) mode to simulate “streaming”. Importantly, ithas built-in integrity protection (integrated authentication: GHASH). It’s like having a smart lock that is fast and has an alarm.
- ChaCha20: is essentially a “high-speed faucet” (stream cipher); when the key is turned (the key), the nonce specifies the position (initialization value), and it flows out “encrypted liquid” (keystream) that mixes with your original data (XOR) to become ciphertext. It also has built-in integrity protection, using a “seal” called Poly1305. The best part is that this algorithm operates in a regular manner (fixed memory access, primarily bit operations),making it very difficult to eavesdrop on its operations (side-channel attacks).
Why did TLS 1.3 decisively eliminate AES-CBC?
The core reason can be summed up in two words:High risk!
- “Padding” is a major vulnerability: CBC must cut data into complete blocks, and if not enough, padding is required. Hackers can exploit the server’s check on whether the padding is correct. The classicPOODLE attack relies on this vulnerability, slowly prying open the lock and extracting encrypted information (padding oracle attack). This effectively gives bad actors repeated trial-and-error opportunities to eavesdrop. The table also clearly indicates its susceptibility to “chosen ciphertext attacks”.
- “Initialization vector” mismanagement can lead to disaster: CBC requires a random initialization vector (IV) for each encryption. If it is not generated properly (repeated or insufficiently random), disaster strikes! For example, the classicBEAST attack exploits this IV to steal information. The table indicates its sensitivity to “physical information leakage”, indirectly suggesting that it has a “loud voice” and is prone to leaks.
- Only encrypting without authentication is too dangerous: CBC only scrambles the information but does not check if it has been maliciously altered. To ensure integrity, an additional “seal” (MAC) is required. The more steps involved, the easier it is to make mistakes (for example, whether to encrypt first or apply MAC first? Reversing the order can be exploited). The table also notes that its “authentication mechanism” requires an “additional MAC message”.
ChaCha20 algorithm actual usage code template

Seeing is believing: Performance differences between ChaCha20 and AES

Based on the MacBook Pro M2 Max chip,Python 3.12.7 environmentactual measurements
Final Thoughts
From the abandonment of AES-CBC in TLS 1.3, we can see that the field of encryption is racing towardsa more concise, more secure (AEAD is standard), and higher performance (hardware-assisted) direction.
Decision basis:
Competing for speed on mainstream platforms? → AES-GCM-256
Competing for stability in interference-prone environments? → ChaCha20-Poly1305
Follow me to explore more practical applications of cryptography!

Complete test code attached:
from __future__ import annotations
"""AES-GCM 256 vs ChaCha20-Poly1305 encryption algorithm comparison demonstration
This script demonstrates the usage, performance differences, and security features of two modern authenticated encryption algorithms"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCM , ChaCha20Poly1305
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
import os
import timeit
from typing import Tuple , Callable , Any , Optional
def generate_random_data(size: int) -> bytes:
"""
Generate random binary data of specified size
Parameters:
size: Size of the random data to generate (in bytes)
Returns:
Randomly generated binary data
"""
if size <= 0 or size % 16 != 0:
size = 32
return os.urandom(size)
def derive_key(password: bytes ,
salt: Optional[ bytes ] = None ,
key_length: int = 32) -> bytes:
"""
Derive a secure key from a password using HKDF
HKDF (HMAC-based Key Derivation Function) is a secure key derivation method
that can convert weak passwords (like user-input passwords) into strong cryptographic keys
Parameters:
password: Original password (in binary format)
salt: Optional salt (to increase randomness of the derived key)
key_length: Length of the key to generate (in bytes)
Returns:
Derived secure key
"""
# If no salt is provided, generate a random salt
if salt is None:
salt = os.urandom(16)
# Create HKDF instance
# Algorithm: SHA-256, key length, salt, context info, backend
hkdf = HKDF(
algorithm = hashes.SHA256() ,
length = key_length ,
salt = salt ,
info = b'key-derivation' ,
backend = default_backend() ,
)
# Derive key from password
return hkdf.derive(password)
def aes_gcm_encrypt_decrypt(key: bytes ,
data: bytes ,
associated_data: bytes) -> bytes:
"""
AES-GCM 256 algorithm encryption and decryption process
Steps:
1. Generate random nonce (12 bytes)
2. Initialize AES-GCM with the key
3. Encrypt data using nonce and associated data
4. Decrypt data using the same nonce and key
Parameters:
key: Encryption key (32 bytes)
data: Data to encrypt
associated_data: Associated data (authenticated but not encrypted)
Returns:
Decrypted original data (for verifying algorithm correctness)
"""
# Generate random nonce (96 bits, which is the standard recommended length for AES-GCM)
nonce = os.urandom(12)
# Create AES-GCM cipher instance
aes_gcm = AESGCM(key)
# Encryption operation: encrypt original data using nonce and associated data
# Output = encrypted data + authentication tag (16 bytes)
ciphertext = aes_gcm.encrypt(nonce , data , associated_data)
# Decryption operation: decrypt using the same nonce and associated data
decrypted_data = aes_gcm.decrypt(nonce , ciphertext , associated_data)
return decrypted_data
def cha_cha20_encrypt_decrypt(key: bytes ,
data: bytes ,
associated_data: bytes) -> bytes:
"""
ChaCha20-Poly1305 algorithm encryption and decryption process
Steps:
1. Generate random nonce (12 bytes)
2. Initialize ChaCha20-Poly1305 with the key
3. Encrypt data using nonce and associated data
4. Decrypt data using the same nonce and key
Parameters:
key: Encryption key (32 bytes)
data: Data to encrypt
associated_data: Associated data (authenticated but not encrypted)
Returns:
Decrypted original data (for verifying algorithm correctness)
"""
# Generate random nonce (96 bits, which is the standard length for ChaCha20-Poly1305)
nonce = os.urandom(12)
# Create ChaCha20-Poly1305 cipher instance
cha_cha = ChaCha20Poly1305(key)
# Encryption operation: encrypt original data using nonce and associated data
# Output = encrypted data + authentication tag (16 bytes)
ciphertext = cha_cha.encrypt(nonce , data , associated_data)
# Decryption operation: decrypt using the same nonce and associated data
decrypted_data = cha_cha.decrypt(nonce , ciphertext , associated_data)
return decrypted_data
def performance_test(algorithm_func: Callable[ [ bytes , bytes , bytes ] , bytes ] ,
key: bytes ,
data: bytes ,
associated_data: bytes ,
iterations: int = 2000) -> float:
"""
Execute performance test for encryption algorithm
Method:
1. Create a function to execute a single encryption-decryption operation
2. Warm-up (execute a few operations)
3. Use timeit to measure the average time for multiple operations
Parameters:
algorithm_func: The algorithm function to test
key: Encryption key
data: Test data
associated_data: Associated data
iterations: Number of test iterations
Returns:
Average time per operation (in milliseconds)
"""
# Create test function (no parameters, calls target algorithm)
def test_wrapper() -> bytes:
return algorithm_func(key , data , associated_data)
# Warm-up: run 5 times to avoid initial overhead affecting test results
for _ in range(5):
_ = test_wrapper()
# Execute performance test
time_taken = timeit.timeit(test_wrapper , number = iterations)
# Calculate average time per operation (milliseconds)
return (time_taken / iterations) * 1000
def demo_encryption_process(algorithm_name: str ,
algorithm: Any ,
data: bytes ,
associated_data: bytes) -> Tuple[ bytes , bytes ]:
"""
Demonstrate the encryption process of a single algorithm
Parameters:
algorithm_name: Algorithm name (for printing)
algorithm: Cipher algorithm instance (AES-GCM-256 or ChaCha20Poly1305)
key: Encryption key
data: Data to encrypt
associated_data: Associated data
Returns:
nonce: Random value used
ciphertext: Encrypted data (including authentication tag)
"""
print(f"\n{algorithm_name} encryption demonstration:")
print(f"Original data: {data.decode()}")
# Generate random nonce
nonce = os.urandom(12)
# Encrypt data
ciphertext = algorithm.encrypt(nonce , data , associated_data)
print(f"Encryption result (ciphertext+tag): {ciphertext.hex()}")
return nonce , ciphertext
def tampering(algorithm_name: str ,
algorithm: Any ,
nonce: bytes ,
ciphertext: bytes ,
associated_data: bytes):
"""
Test the tampering detection capability of the data
Method:
1. Modify one byte of the ciphertext
2. Attempt to decrypt the modified ciphertext
3. Capture and report verification failure error
Parameters:
algorithm_name: Algorithm name (for printing)
algorithm: Cipher algorithm instance
nonce: Nonce used for original encryption
ciphertext: Original ciphertext
associated_data: Associated data
"""
print(f"\nTesting tampering detection for {algorithm_name}...")
try:
# Create a modifiable copy of the ciphertext
tampered_ciphertext = bytearray(ciphertext)
# Modify one byte (XOR modification at position 10)
tampered_ciphertext[ 10 ] ^= 0x01
# Attempt to decrypt the tampered data
algorithm.decrypt(nonce , bytes(tampered_ciphertext) , associated_data)
# If successful, tampering detection failed (this should not happen)
print("Warning: Tampering was not detected!")
except Exception as e:
# An exception will be raised on verification failure
print(f"{algorithm_name} authentication failed: {type(e).__name__} - {str(e)}")
def main_encryption_decryption():
"""Main function: coordinates the entire demonstration process"""
# 1. Prepare test data and parameters
password = b"my-secret-password" # Original password (should be more complex in actual applications)
associated_data = b"authenticated-but-not-encrypted" # Associated data (authenticated but not encrypted)
data_size = 1024 * 1024 # 1MB test data
test_data = generate_random_data(data_size)
print(f"Test data size: {data_size / 1024:.2f} KB")
# 2. Derive a secure encryption key from the password (32 bytes = 256 bits)
key = derive_key(password)
print(f"Using key: {key.hex()}")
# 3. Performance test
# Test AES-GCM-256
print("\nPerformance test - AES-GCM-256:")
aes_time = performance_test(aes_gcm_encrypt_decrypt , key , test_data , associated_data)
print(f"Encryption/Decryption time: {aes_time:.4f} milliseconds")
# Test ChaCha20-Poly1305
print("\nPerformance test - ChaCha20-Poly1305:")
cha_cha_time = performance_test(cha_cha20_encrypt_decrypt , key , test_data , associated_data)
print(f"Encryption/Decryption time: {cha_cha_time:.4f} milliseconds")
# Performance comparison
if aes_time < cha_cha_time:
ratio = cha_cha_time / aes_time
print(f"\nPerformance comparison: AES-GCM is {ratio:.3f}x faster than ChaCha20-Poly1305")
else:
ratio = aes_time / cha_cha_time
print(f"\nPerformance comparison: ChaCha20-Poly1305 is {ratio:.3f}x faster than AES-GCM")
# 4. Demonstration of encryption process
small_data = b"Hello, AEAD algorithms!" # Small data for demonstration
# Create algorithm instances
aes_gcm_256 = AESGCM(key)
cha_cha_256 = ChaCha20Poly1305(key)
# Demonstrate AES-GCM encryption
aes_nonce , aes_ciphertext = demo_encryption_process("AES-GCM" ,
aes_gcm_256 ,
small_data ,
associated_data)
# Demonstrate ChaCha20-Poly1305 encryption
cha_cha_nonce , cha_cha_ciphertext = demo_encryption_process("ChaCha20-Poly1305" ,
cha_cha_256 ,
small_data ,
associated_data)
# 5. Decryption verification
aes_decrypted = aes_gcm_256.decrypt(aes_nonce , aes_ciphertext , associated_data)
cha_cha_decrypted = cha_cha_256.decrypt(cha_cha_nonce , cha_cha_ciphertext , associated_data)
print(f"\nAES-GCM decryption result: {aes_decrypted.decode()}")
print(f"ChaCha20-Poly1305 decryption result: {cha_cha_decrypted.decode()}")
# 6. Tampering test
tampering("AES-GCM" , aes_gcm_256 , aes_nonce , aes_ciphertext , associated_data)
tampering("ChaCha20-Poly1305" , cha_cha_256 , cha_cha_nonce , cha_cha_ciphertext , associated_data)
if __name__ == "__main__":
main_encryption_decryption()
