Implementing AES/ECB/PKCS5 padding in Python

12.3k views Asked by At

I am trying to implement a python program to encrypt a plain text using AES/ECB/PKCS5 padding. The output I am getting is slightly different from expected.

Python3 program:

import base64
from Crypto.Cipher import AES

 
def add_to_16(value):
    while len(value) % 16 != 0:
        value += '\0'
    return str.encode (value) # returns bytes
 

# Encryption method
def encrypt(text):
         # Secret key 
    key='92oifgGh893*cj%7' 

         # Text to be encrypted
         # Initialize encryptor
    aes = AES.new(key, AES.MODE_ECB) 

         # Aes encryption to be
    encrypt_aes = aes.encrypt(add_to_16(text)) 

         # Converted into a string with base64
    encrypted_text = str(base64.encodebytes (encrypt_aes), encoding = 'utf-8')

    print(encrypted_text)
    return encrypted_text

if __name__ == '__main__': 

    text = '{  "Message": "hello this is a plain text" , "user":"john.doe", "Email":"[email protected]}'
    entrypted_text = encrypt(text)

The output for above program is:

oo8jwHQNQnBwVUsJ5piShFRM3PFFIfULwcoFOEQhPMTAvexSr6eE9aFLVQTpAKBFkGi8vNbtScvyexSxHBlwVapJ5Szz1JPR9q9cHHJYYMzGocln4TRPFQ6S3e8jjVud

where as when verified with 3rd party tools online, the results is:

oo8jwHQNQnBwVUsJ5piShFRM3PFFIfULwcoFOEQhPMTAvexSr6eE9aFLVQTpAKBFkGi8vNbtScvyexSxHBlwVapJ5Szz1JPR9q9cHHJYYMwnIIuNCUVn/IExpxebqXV1

Can someone please guide me where I am doing wrong?

6

There are 6 answers

0
NaniK On BEST ANSWER

I have framed the code with below for padding with PKCS5 and is working as expected.

block_size=16
pad = lambda s: s + (block_size - len(s) % block_size) * chr(block_size - len(s) % block_size)

and the encrypt method was re-written as below:

def encrypt(plainText,key):
    
    aes = AES.new(key, AES.MODE_ECB)    
    encrypt_aes = aes.encrypt(pad(plainText))   
    encrypted_text = str(base64.encodebytes (encrypt_aes), encoding = 'utf-8')
    return encrypted_text
0
Henno Brandsma On

PKCS 5 (or 7) padding is not adding 0 bytes, but adding a c byteswith valuec(where1 <= c <= 16) if you're c` bytes short of a block length multiple.

So if you already have a multiple of 16, add a full 16 bytes of value 16, and if your last block is 'stop' (4 bytes), we add 12 bytes with value 0xc (12 in hex) to fill up the block. Etc.

This way the receiver (after decryption of the final block) can check the last byte c and check if the value is 1 <= c <= 16 (if not, reject the decryption) and then check that the last c bytes indeed are all that same value, and then remove them from the decryption. This way the receiver does not have to guess how many bytes of the last block were only padding or really part of the plain text. It's unambiguous doing it the PKCS way.

I'll leave the coding up to you.

1
FDX SOFT SA DE CV On

Just change this

    def add_to_16(value):
        while len(value) % 16 != 0:
        value += '\a'
        return str.encode(value)

1
Laerte On

You can use aes-pkcs5 package. I'm the author, it uses cryptography package instead of outdated pycrypto used in others answers and is compatible with Python 3.8+.

You can install via pip:

pip install aes-pkcs5

The same code that you posted using aes-pkcs5:

from aes_pkcs5.algorithms.aes_ecb_pkcs5_padding import AESECBPKCS5Padding

key = "92oifgGh893*cj%7"

cipher = AESECBPKCS5Padding(key, "b64")
text = '{  "Message": "hello this is a plain text" , "user":"john.doe", "Email":"[email protected]}'
encrypted_text = cipher.encrypt(text)
2
Sumit Kumar Singh On

Here is the complete code, in case if anyone is still looking.

tested against:
  • python3.6
  • python3.8

** used pycryptodome

  • encrypt_aes.py
import hashlib
from Crypto.Cipher import AES
import base64 

class AES_pkcs5:
    def __init__(self,key:str, mode:AES.MODE_ECB=AES.MODE_ECB,block_size:int=16):
        self.key = self.setKey(key)
        self.mode = mode
        self.block_size = block_size

    def pad(self,byte_array:bytearray):
        """
        pkcs5 padding
        """
        pad_len = self.block_size - len(byte_array) % self.block_size
        return byte_array + (bytes([pad_len]) * pad_len)
    
    # pkcs5 - unpadding 
    def unpad(self,byte_array:bytearray):
        return byte_array[:-ord(byte_array[-1:])]


    def setKey(self,key:str):
        # convert to bytes
        key = key.encode('utf-8')
        # get the sha1 method - for hashing
        sha1 = hashlib.sha1
        # and use digest and take the last 16 bytes
        key = sha1(key).digest()[:16]
        # now zero pad - just incase
        key = key.zfill(16)
        return key

    def encrypt(self,message:str)->str:
        # convert to bytes
        byte_array = message.encode("UTF-8")
        # pad the message - with pkcs5 style
        padded = self.pad(byte_array)
        # new instance of AES with encoded key
        cipher = AES.new(self.key, AES.MODE_ECB)
        # now encrypt the padded bytes
        encrypted = cipher.encrypt(padded)
        # base64 encode and convert back to string
        return base64.b64encode(encrypted).decode('utf-8')

    def decrypt(self,message:str)->str:
        # convert the message to bytes
        byte_array = message.encode("utf-8")
        # base64 decode
        message = base64.b64decode(byte_array)
        # AES instance with the - setKey()
        cipher= AES.new(self.key, AES.MODE_ECB)
        # decrypt and decode
        decrypted = cipher.decrypt(message).decode('utf-8')
        # unpad - with pkcs5 style and return 
        return self.unpad(decrypted)
        
if __name__ == '__main__':
    # message to encrypt 
    message = 'hello world'
    secret_key = "65715AC165715AC165715AC165715AC1"
    AES_pkcs5_obj = AES_pkcs5(secret_key)
    
    encrypted_message = AES_pkcs5_obj.encrypt(message)

    print(encrypted_message)
    decrypted_message = AES_pkcs5_obj.decrypt(encrypted_message)
    print(decrypted_message)

Output:

>>> python encrypt_aes.py
>>> PDhIFEVqLrJiZQC90FPHiQ== # encrypted message
>>> hello world # and the decrypted one

I had tested many already available codes but none of them gave exact encryption as java ones. So, this is combined of all the found blogs and early written code compatible with python2

0
Ryan Anguiano On

You can use a random string separated by a null byte for the padding to add a bit of randomness sometimes.

import random
import string

from Crypto.Cipher import AES

NULL_BYTE = '\x00'


def random_string(size: int) -> str:
    return ''.join([
        random.choice(string.printable) for _ in range(size)
    ])


def encode_aes(value: str, key: str) -> bytes:
    cipher = AES.new(key[:32], AES.MODE_ECB)
    mod = len(value) % cipher.block_size
    padding = (cipher.block_size - mod) % cipher.block_size
    if padding > 0:
        value += NULL_BYTE + random_string(padding - 1)
    return cipher.encrypt(value)


def decode_aes(value: bytes, key: str) -> str:
    cipher = AES.new(key[:32], AES.MODE_ECB)
    decrypted = cipher.decrypt(value).decode('utf8')
    return decrypted.rsplit(NULL_BYTE, 1)[0]