Implementing AES/ECB/PKCS5 padding in Python
Asked Answered
H

6

2

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?

Hennebery answered 5/10, 2020 at 7:8 Comment(3)
Welcome to Stackoverflow. Your "add_to_16" is adding (hex) x00's to the plaintext and this named "ZeroPadding". As your reference is "PKCS5Padding" (or PKCS7Padding, depending on the programming anguage) you need to change your function to receive this kind of padding. Security warning: do not use UNSECURE ECB-mode any longer.Apc
The online tools are right of course.Ineducation
You need a tool that also allows you to choose the padding, e.g. here. However, this tool expects the key hex encoded (39326f69666747683839332a636a2537). PKCS7 is generally more reliable than Zero padding, so PKCS7 is preferable.Pileate
H
1

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
Hennebery answered 11/10, 2020 at 16:46 Comment(0)
J
3

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

Jessi answered 24/12, 2021 at 14:35 Comment(2)
Your code is clean and simple. I like how you do it. This helped me a lotMurphree
You didn't user bytearray() to convert bytes to bytearray. why? byte_array = bytearray(message.encode("UTF-8"))Gause
W
2

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)
Wive answered 12/6, 2022 at 3:23 Comment(1)
This really came in handy for what I needed exactly. Thank youNerti
H
1

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
Hennebery answered 11/10, 2020 at 16:46 Comment(0)
I
0

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.

Ineducation answered 5/10, 2020 at 7:39 Comment(0)
T
0

Just change this

    def add_to_16(value):
        while len(value) % 16 != 0:
        value += '\a'
        return str.encode(value)
Thoracotomy answered 27/10, 2023 at 16:46 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Toddy
H
-1

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]

Hoxie answered 6/7, 2022 at 3:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.