44from cryptography .hazmat .primitives .kdf .pbkdf2 import PBKDF2HMAC
55import os
66import base64
7-
87class AESCrypto :
98 ALGORITHM = algorithms .AES
109 IV_LENGTH = 16
1110 KEY_LENGTH = 32 # 256 bits
12-
11+ BLOCK_SIZE = 16 # AES block size
12+ class InvalidPaddingError (Exception ):
13+ pass
1314 @staticmethod
1415 def prepare_key (key : str ) -> bytes :
1516 # Hash the key using SHA-256 to ensure it's always the correct length (32 bytes)
1617 digest = hashes .Hash (hashes .SHA256 (), backend = default_backend ())
1718 digest .update (key .encode ('utf-8' ))
1819 return digest .finalize ()
19-
20+ @staticmethod
21+ def pkcs7_pad (data : bytes ) -> bytes :
22+ """Apply PKCS7 padding to the data."""
23+ padding_length = AESCrypto .BLOCK_SIZE - (len (data ) % AESCrypto .BLOCK_SIZE )
24+ padding = bytes ([padding_length ] * padding_length )
25+ return data + padding
26+ @staticmethod
27+ def pkcs7_unpad (data : bytes ) -> bytes :
28+ """
29+ Remove and validate PKCS7 padding.
30+ Raises InvalidPaddingError if padding is incorrect.
31+ """
32+ if len (data ) == 0 :
33+ raise AESCrypto .InvalidPaddingError ("Empty data" )
34+ if len (data ) % AESCrypto .BLOCK_SIZE != 0 :
35+ raise AESCrypto .InvalidPaddingError ("Data length not multiple of block size" )
36+ padding_length = data [- 1 ]
37+ if padding_length == 0 or padding_length > AESCrypto .BLOCK_SIZE :
38+ raise AESCrypto .InvalidPaddingError ("Invalid padding length" )
39+ if len (data ) < padding_length :
40+ raise AESCrypto .InvalidPaddingError ("Padding length larger than data" )
41+ # Check all padding bytes are correct
42+ padding = data [- padding_length :]
43+ if not all (x == padding_length for x in padding ):
44+ raise AESCrypto .InvalidPaddingError ("Invalid padding values" )
45+ return data [:- padding_length ]
2046 @staticmethod
2147 def encrypt (data : str , key : str ) -> dict :
2248 # Generate a random Initialization Vector (IV)
2349 iv = os .urandom (AESCrypto .IV_LENGTH )
2450 key_buffer = AESCrypto .prepare_key (key )
25-
2651 # Create cipher and encrypt the data
2752 cipher = Cipher (AESCrypto .ALGORITHM (key_buffer ), modes .CBC (iv ), backend = default_backend ())
2853 encryptor = cipher .encryptor ()
29-
30- # Pad data to be multiple of 16 bytes (block size for AES)
31- padding_length = 16 - (len (data ) % 16 )
32- padded_data = data + chr (padding_length ) * padding_length
33- encrypted = encryptor .update (padded_data .encode ('utf-8' )) + encryptor .finalize ()
34-
54+ # Properly pad data using PKCS7
55+ padded_data = AESCrypto .pkcs7_pad (data .encode ('utf-8' ))
56+ encrypted = encryptor .update (padded_data ) + encryptor .finalize ()
3557 # Return the encrypted content and the IV (both base64 encoded)
3658 return {
3759 'content' : base64 .b64encode (encrypted ).decode ('utf-8' ),
3860 'iv' : base64 .b64encode (iv ).decode ('utf-8' )
3961 }
40-
4162 @staticmethod
4263 def decrypt (encrypted_data : dict , key : str ) -> str :
43- # Decode the base64 encoded IV and content
44- iv = base64 .b64decode (encrypted_data ['iv' ])
45- encrypted_content = base64 .b64decode (encrypted_data ['content' ])
46- key_buffer = AESCrypto .prepare_key (key )
47-
48- # Create cipher and decrypt the data
49- cipher = Cipher (AESCrypto .ALGORITHM (key_buffer ), modes .CBC (iv ), backend = default_backend ())
50- decryptor = cipher .decryptor ()
51-
52- decrypted = decryptor .update (encrypted_content ) + decryptor .finalize ()
53-
54- # Remove padding
55- padding_length = decrypted [- 1 ]
56- decrypted = decrypted [:- padding_length ]
57-
58- return decrypted .decode ('utf-8' )
64+ try :
65+ # Decode the base64 encoded IV and content
66+ iv = base64 .b64decode (encrypted_data ['iv' ])
67+ encrypted_content = base64 .b64decode (encrypted_data ['content' ])
68+ key_buffer = AESCrypto .prepare_key (key )
69+ # Create cipher and decrypt the data
70+ cipher = Cipher (AESCrypto .ALGORITHM (key_buffer ), modes .CBC (iv ), backend = default_backend ())
71+ decryptor = cipher .decryptor ()
72+ decrypted = decryptor .update (encrypted_content ) + decryptor .finalize ()
73+ # Remove padding with validation
74+ unpadded = AESCrypto .pkcs7_unpad (decrypted )
75+ return unpadded .decode ('utf-8' )
76+ except AESCrypto .InvalidPaddingError as e :
77+ # Handle padding errors uniformly to prevent timing attacks
78+ raise AESCrypto .InvalidPaddingError ("Decryption failed" )
79+ except Exception as e :
80+ # Handle all other errors uniformly
81+ raise AESCrypto .InvalidPaddingError ("Decryption failed" )
0 commit comments