Cryptography Lesson: Notebook 1 — AES ECB (Electronic Codebook)
Symmetric Block Cipher
Cryptography Lesson: Notebook 1 — AES ECB (Electronic Codebook)
Symmetric Block Cipher
Cryptology uses mathematical methods to keep information secret and protect its integrity. The most common of these methods is symmetric key encryption. In symmetric encryption, the same key is used for both encryption and decryption.
Symmetric key encryption algorithms are often divided into two subtypes:
- block ciphers
- stream ciphers
Block encryption is an algorithm that takes fixed-length pieces of data (blocks) and converts them into encrypted blocks of the same length using a key. Block ciphers take in a fixed-length message, a private key, and they produce a ciphertext that is the same length as the fixed-length plaintext message.
Formal definition of Block Cipher:

k: secret key
E_k: encryption function
n: block size
AES and Triple DES (Data Encryption Standard) are the most common block ciphers in use today. DES is still interesting to study, but due to its small 56-bit key size, it is considered insecure. AES (Advanced Encryption Standard) is the industry standard today and has both software and hardware acceleration support worldwide.
Block ciphers can only encrypt one block at a time, whereas in the real world, messages are often much longer. That is why “modes of operation” have been developed.
For note:
- ECB (Electronic Codebook): Each block is encrypted independently. It is not secure.
- CBC (Cipher Block Chaining): Each block is chained with the previous encrypted block.
- CTR (Counter Mode): Counter values are encrypted and XORed; this allows for parallelism.
- GCM (Galois/Counter Mode): CTR + authentication = AEAD (confidentiality + integrity).
These modes provide confidentiality, integrity, and advanced security features that block ciphers alone cannot provide. These methods will be discussed later in the tutorial series.
AES-ECB (Electronic Codebook)
In cryptography, block ciphers are the basic building blocks that encrypt fixed-size data (usually 128 bits) under a specific key. AES (Advanced Encryption Standard) is the most widely used block cipher algorithm today. However, the security of AES depends on the mode of operation used. These modes extend the limited capacity of the block cipher alone, enabling the secure encryption of long data.
ECB (Electronic Codebook) is the simplest of these modes and is historically one of the first examples of block cipher usage. However, its use is strongly discouraged in modern cryptography applications due to security concerns. This mode is mostly used for training and testing purposes.
In ECB mode, the plaintext is divided into blocks and each block is encrypted independently with the AES algorithm.

P_i: clear text block (16 bytes)
C_i: encrypted block
E_k: AES encryption function with key k
Since each block is encrypted with the same key, the same plaintext block always translates into the same ciphertext block. Every 16 bytes of plaintext has a corresponding 16-byte output.

Advantages:
- Simplicity: Easy to implement and understand.
- Suitable for parallel processing: Because blocks are encrypted independently, it provides a speed advantage in parallel architectures.
Disadvantages:
- Preserves patterns: Repeated plaintext blocks are also repeated in the ciphertext.
- Leaks structural information: Especially in structured data such as images, databases, or formatted text, data patterns can still be seen in the encrypted form.
- Does not ensure encryption integrity: An attacker can manipulate the message by rearranging the encrypted blocks (cut-and-paste attack).
- Prohibited by modern standards: Not used in security protocols such as ECB, TLS, IPsec, and SSH.
I will give you an example of ECB mode with python:
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backend
import os
encryptionKEY = os.urandom(16) # 128 bits
AESEngine = Cipher(
algorithm=algorithms.AES(encryptionKEY),
mode=modes.ECB(),
backend=default_backend()
)
encryptor = AESEngine.encryptor()
decryptor = AESEngine.decryptor()
message = b"0000000000000000"
print(f"LENGTH OF MESSAGE: {len(message)}\n")
# The update functions for both encryption and decryption always work on 16 bytes at a time.
## Calling update with fewer than 16 bytes produces no immediate result.
### Once 16 or more bytes are available, as many 16-byte blocks of ciphertext as possible are produced.
cipher = encryptor.update(message) + encryptor.finalize()
print(f"CIPHER LENGTH: {len(cipher.hex())}\n")
decrypted = decryptor.update(cipher) + decryptor.finalize()
print(f"PLAIN: {decrypted}\n")
The fundamental weakness of the AES-ECB encryption mode is that identical plaintext blocks are converted into identical ciphertext blocks. This feature leads to significant information leakage, especially when used on images or repetitive data. Patterns in the image are still visible in the encrypted data through the block structure.
The code provides concrete visuals showing why ECB mode is unsafe in practice:
"""
The fundamental weakness of the AES-ECB encryption mode is that identical plaintext blocks are converted into identical ciphertext blocks.
This feature leads to significant information leakage, especially when used on images or repetitive data.
Patterns in the image are still visible in the encrypted data through the block structure.
The code provides concrete visuals showing why ECB mode is unsafe in practice.
"""
import numpy as np
from PIL import Image
import os,hashlib
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
BLOCKSIZE = 16 # AES block size in bytes (and we'll align pixels to this)
KEY = os.urandom(BLOCKSIZE)
IMAGEPATH = os.path.join(os.getcwd(),"samples","lock_image.jpg")
SAVEPATH = os.path.join(os.getcwd(),"samples","ECB_encrypted_lock_image.jpg")
def LoadGrayscale(path:str)->np.ndarray:
"""
Load an image file as uint8 grayscale (H, W).
"""
grayIMG = Image.open(path).convert("L") # grayscale
arrayIMG = np.array(grayIMG,dtype=np.uint8)
return arrayIMG
def PaddingImageBlock(image:np.ndarray,blockSize:int=BLOCKSIZE)->np.ndarray:
"""
Pad image on bottom/right with zeros so that width*height is a multiple of 16.
Additionally, pad width to a multiple of 16 so that 1 block == 16 horizontal pixels.
Since each pixel of the image = 1 byte, 1 block = 16 pixels.
"""
height,width = image.shape
newWidth = ((width+blockSize-1)//blockSize)*blockSize
newHeight = ((height+1)//1)*1 # keep rows as-is (pixel = 1 byte)
# Ensure total bytes multiple of 16
totalBytes = newWidth*newHeight
if totalBytes % blockSize != 0:
# make height multiple of block if needed
newHeight = ((newHeight+blockSize-1)//blockSize)*blockSize
padHeight = max(0,newHeight-height)
padWidth = max(0,newWidth-width)
if padHeight or padWidth:
padded = np.zeros((newHeight,newWidth),dtype=np.uint8)
padded[:height,:width] = image
return padded
return image
def AESEncryptECB(bytesIN:bytes,key:bytes)->bytes:
AESEngine = Cipher(
algorithm=algorithms.AES(key),
mode=modes.ECB(),
backend=default_backend()
)
encryptor = AESEngine.encryptor()
cipher = encryptor.update(bytesIN)+encryptor.finalize()
return cipher
def BlocksToToneImage(cipherBytes:bytes,height:int,width:int,blockSize:int=BLOCKSIZE)->np.ndarray:
"""
Map each 16-byte block to a grayscale tone (0..255).
Identical blocks -> identical tone. Returns (H, W) uint8 image.
"""
assert len(cipherBytes) == height*width,"Ciphertext length must equal H*W for 1 byte per pixel."
blockNumbers = len(cipherBytes)//blockSize
tones = np.zeros(blockNumbers,dtype=np.uint8)
for idx in range(blockNumbers):
bulk = cipherBytes[idx*blockSize:(idx+1)*blockSize]
# Deterministic mapping: first byte of SHA-256 hash
tones[idx] = hashlib.sha256(bulk).digest()[0]
# It copies each tone 16 times in succession, producing a sequence that matches the original block size.
expanded = np.repeat(tones,blockSize).astype(np.uint8) # Since each pixel of the image = 1 byte, 1 block = 16 pixels.
return expanded.reshape((height,width))
def SaveImageU8(path:str,arrayIMG:np.ndarray)->None:
Image.fromarray(arrayIMG,mode="L").save(path)
imageArray = LoadGrayscale(IMAGEPATH)
imagePadded = PaddingImageBlock(imageArray,BLOCKSIZE)
height,width = imagePadded.shape
# It converts a matrix to a one-dimensional array.
imageFlattenBytes = imagePadded.flatten().tobytes() # Padded 2D image matrix (NumPy array of dimensions H × W).
# The AES algorithm works with a raw byte array, not a 2D image.
imageEncryptedECB = AESEncryptECB(bytesIN=imageFlattenBytes,key=KEY)
toneECB = BlocksToToneImage(cipherBytes=imageEncryptedECB,height=height,width=width,blockSize=BLOCKSIZE)
SaveImageU8(path=SAVEPATH,arrayIMG=toneECB)
You can replace the image used here with a different image of your choice.
You can access the full code on Github:
https://github.com/BrsDincer/Cryptography-Lesson-Notebooks
You can donate to contribute to these efforts and share more:
https://kreosus.com/silentiusnet
Thank you.
메타데이터
- post_id
- 5baabcedfa59
- slug
- cryptography-lesson-notebook-1-aes-ecb-electronic-codebook-5baabcedfa59
- url
- https://medium.com/@brsdncr/cryptography-lesson-notebook-1-aes-ecb-electronic-codebook-5baabcedfa59
- canonical_url
- https://medium.com/@brsdncr/cryptography-lesson-notebook-1-aes-ecb-electronic-codebook-5baabcedfa59
- author_url
- https://medium.com/@brsdncr
- status
- ok
- fetched_at
- 2026-06-24 23:31:39