I created this software in python3. The problem is that my software don't write the result if I want to decrypt a file.
I am using easygui for gui as a gui. Finally I don't have any error message. The software just close as usually but the file stay encrypted.
def decrypt(key, filename):
chunksize = 64 * 1024
outputFile = filename[11:]
with open(filename, 'rb') as infile:
filesize = int(infile.read(16))
IV = infile.read(16)
decryptor = AES.new(key, AES.MODE_CBC, IV)
with open(outputFile, 'wb') as outfile:
while True:
chunk = infile.read(chunksize)
if len(chunk) == 0:
break
outfile.write(decryptor.decrypt(chunk))
outfile.truncate(filesize)
def getKey(password):
hasher = SHA256.new(password.encode('utf-8'))
return hasher.digest()
def Main():
image = "./images/encryption.gif"
msg = "Do you want to encrypt or decrypt a file ?"
choices = ["Encrypt", "Decrypt", "Exit"]
reply = buttonbox(msg, image=image, choices=choices)
if reply == 'Encrypt':
filename = fileopenbox(msg="Select the file to Encrypt",title="Select the file to Encrypt", default='*', filetypes=None, multiple=False)
password = passwordbox(msg="Enter a password",title="Enter a password",default="")
encrypt(getKey(password),filename)
elif reply == 'Decrypt':
filename = fileopenbox(msg="Select the file to Decrypt", title="Select the file to Decrypt", default='*', filetypes=None, multiple=False)
password = passwordbox(msg="Enter a password",title="Enter a password",default="")
decrypt(getKey(password),filename)
else:
sys.exit(0)
if __name__ == '__main__':
Main()
You have forgotten to take the requirements of AESCipher.encrypt and AESCipher.decrypt into consideration: you need to perform the required padding yourself:
For MODE_ECB, MODE_CBC, and MODE_OFB, plaintext length (in bytes) must be a multiple of block_size.
Probably you will miss part of the ciphertext if you do not provide a message (file contents) that is exactly a multiple of 16 bytes as some part of the message stays in cache. So zero to 15 bytes may be stripped off of the input file each time you perform encryption.
Obviously for very small files up to 15 bytes nothing will be left.
Related
Working with processing data that I receive as b64-encoded strings. For some reason I received data the other day where a set of the b64-encoded strings had four white space chars in them, making the decoded output incorrect. I made a small python script to brute force the missing characters that did the job, but I wanted to see if I can improve the speed by multiprocessing.
I'm not that familiar with using generators so it seems like this is throwing me off in the process of making it work together with multiprocessing.
Following sample code isn't the exact code I'm working with, but similar enough.
import base64
from maskprocessor import maskprocessor as maskproc
from tqdm import tqdm
import string
import multiprocessing as mp
NCORE = 8
KEY = b"some decryption key"
printable_chars = set(bytes(string.printable, 'ascii'))
def printable(data):
return True if all(char in printable_chars for char in data) else False
def decrypt(cipher):
try:
plain = base64.b64decode(cipher) #someDecryptAlgo(KEY, base64.b64decode(cipher))
return plain
except:
return None
def slow_generator(ciphers):
def gen_to_queue(input_q, ciphers):
for cipher in ciphers:
input_q.put(cipher)
for _ in range(NCORE):
input_q.put((None,None))
def process(input_q, output_q):
while True:
cipher = input_q.get()
if cipher is None:
output_q.put(None)
break
output_q.put((cipher, decrypt(cipher)))
input_q = mp.Queue(maxsize=NCORE * 2)
output_q = mp.Queue(maxsize=NCORE * 2)
gen_pool = mp.Pool(1, initializer=gen_to_queue, initargs=(input_q, ciphers))
pool = mp.Pool(NCORE, initializer=process, initargs=(input_q, output_q))
finished_workers = 0
while True:
cipher, plain = output_q.get()
if plain is None:
finished_workers += 1
if finished_workers == NCORE:
break
else:
yield cipher, plain
if __name__ == "__main__":
actual_msg = "All your base are belong to us"
actual_data_b64 = b'QWxsIHlvdXIgYmFzZSBhcmUgYmVsb25nIHRvIHVz' # "All your base are belong to us
received_data_b64 = b'QWxsI lvdXIgY FzZSBhcmUg mVsb25n HRvIHVz'
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
n_ws = received_data_b64.decode('utf-8').count(' ')
g = maskproc(received_data_b64.decode('utf-8').replace(' ', '?1'), custom_charset1=charset)
output = slow_generator(g)
for cipher, plain in zip(x[0], x[1]):
if printable(plain):
if plain.decode('utf-8') == actual_msg:
print(f"Found correct cipher: {cipher}")
print(f"Secret message: {plain}")
break
The decoded b64 data is then decrypted using a static key, so the function for checking if I've found the correct bytes is obviously more complex than in the sample above, but I think the sample represents the same problem I got, making the generator work with multiprocessing. There is no reading/writing files involved.
I'd like to avoid creating a list of all the candidates that the maskproc generator creates but so far I haven't been able to make multiprocessing work without doing this, and thus I wonder if anyone got some pointers for how to do this?
Edit: Say I want to run N (4/8/16/etc) processes in parallel, could it be an idea to make a list of length N with candidates that is then processed and in this way avoid having to make a list of length 64^4?
Edit2: Changed code to the one adopted from https://stackoverflow.com/a/58832140/853898
Edit3: Think I managed to get it working. Now to only find a way to implement a tqdm progress bar, if possible.
import base64
from maskprocessor import maskprocessor as maskproc
from tqdm import tqdm
import string
import multiprocessing as mp
NCORE = 8
KEY = b"some decryption key"
printable_chars = set(bytes(string.printable, 'ascii'))
def printable(data):
return True if all(char in printable_chars for char in data) else False
def decrypt(cipher):
try:
plain = base64.b64decode(cipher) #someDecryptAlgo(KEY, base64.b64decode(cipher))
return plain
except:
return None
def slow_generator(ciphers):
def gen_to_queue(input_q, ciphers):
for cipher in ciphers:
input_q.put(cipher)
for _ in range(NCORE):
input_q.put(None)
def process(input_q, output_q):
while True:
cipher = input_q.get()
if cipher is None:
output_q.put((None, None))
break
output_q.put((cipher, decrypt(cipher)))
input_q = mp.Queue(maxsize=NCORE * 2)
output_q = mp.Queue(maxsize=NCORE * 2)
gen_pool = mp.Pool(1, initializer=gen_to_queue, initargs=(input_q, ciphers))
pool = mp.Pool(NCORE, initializer=process, initargs=(input_q, output_q))
finished_workers = 0
while True:
cipher, plain = output_q.get()
if plain is None:
finished_workers += 1
if finished_workers == NCORE:
break
else:
yield cipher, plain
if __name__ == "__main__":
actual_msg = "All your base are belong to us"
actual_data_b64 = b'QWxsIHlvdXIgYmFzZSBhcmUgYmVsb25nIHRvIHVz' # "All your base are belong to us
received_data_b64 = b'QWxsI lvdXIgY FzZSBhcmUg mVsb25n HRvIHVz'
#charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
charset = "HmYI"
n_ws = received_data_b64.decode('utf-8').count(' ')
g = maskproc(received_data_b64.decode('utf-8').replace(' ', '?1'), custom_charset1=charset)
output = slow_generator(g)
for cipher, plain in output:
if printable(plain):
if plain.decode('utf-8') == actual_msg:
print(f"Found correct cipher: {cipher}")
print(f"Secret message: {plain}")
break
I have the following code running like a charm
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
key = b'Sixteen byte key'
data = 'some text to encrypt'.encode("UTF-8")
data = pad(data, AES.block_size)
encryptor = AES.new(key, AES.MODE_CBC)
iv = encryptor.IV
decryptor = AES.new(key, AES.MODE_CBC, IV=iv)
ciphertext = encryptor.encrypt(data)
print(ciphertext)
plaintext = decryptor.decrypt(ciphertext)
print(unpad(plaintext, 16))
But when I try to convert to a function I got an padding error. My adapted code is
def cbc(msg, op):
key = b'Sixteen byte key'
encryptor = AES.new(key, AES.MODE_CBC)
iv = encryptor.IV
decryptor = AES.new(key, AES.MODE_CBC, IV=iv)
if op == 1:
data = msg.encode("UTF-8")
data = pad(data, AES.block_size)
ciphertext = encryptor.encrypt(data)
print(ciphertext)
else:
plaintext = decryptor.decrypt(msg)
print(unpad(plaintext, 16))
My log is
Traceback (most recent call last):
File "D:/Google Drive/max/AES.py", line 48, in <module>
cbc(b'*\xd3\xc1Y\xc2f;\xf0\xc0#\xd9E\xc5x\x11\xb4', 2)
File "D:/Google Drive/max/AES.py", line 19, in cbc
print(unpad(plaintext, 16))
File "C:\Users\Evilmaax\AppData\Local\Programs\Python\Python36\lib\site-packages\Crypto\Util\Padding.py", line 90, in unpad
raise ValueError("Padding is incorrect.")
ValueError: Padding is incorrect.
Error occurs in else statement when I try to decrypt a byte message like *\xd3\xc1Y\xc2f;\xf0\xc0#\xd9E\xc5x\x11\xb4. Important: This message was generated by the if statement of same function.
Does anyone know why this happens?
__________________________________________________________
EDIT: Using Rob Napier's tip (thank you so much) I solved the problem. If you experiencing the same issue, this is a functional version:
def cbc(key, data, op):
if op == 1:
cipher = AES.new(key, AES.MODE_CBC, iv)
data = key.encrypt(data)
print(f"Coded text: {data}")
else:
decipher = AES.new(key, AES.MODE_CBC, IV=iv)
print(f'Plaintext: {unpad(decipher.decrypt(data), BLOCK_SIZE).decode("utf-8")}')
You're generating a random IV, which is good, but you then throw it away, which means you can't decrypt the data.
This line creates an encryptor with a random IV:
encryptor = AES.new(key, AES.MODE_CBC)
This line creates a decryptor with the same IV:
decryptor = AES.new(key, AES.MODE_CBC, IV=iv)
That's fine in your initial code, but you don't save the IV anywhere, so when you put it in a function which you call twice, you're trying to decrypt with a different IV than you encrypted with.
You need to save the IV as part of the cipher text, and use it during decryption. If you want an example of such a format, see the RNCryptor spec, and if you want to see an example in Python, see RNCryptor-python.
For some reason, the code that says:
private_key = RSA.import_key(open(privdirec).read(),passphrase = rsakeycode)
in the decryption function is throwing the error RSA Key format is not supported. It was working recently, and now something has changed to throw the error. Could anyone take a look at my code snippets and help?
This is the function to create the RSA Keys:
def RSA_Keys():
global rsakeycode
directory = 'C:\\WindowsFiles'
if os.path.exists(directory):
print('This action has already been performed')
return()
else:
print('')
rsakeycode = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
f = open('keycode.txt', 'w+')
f.write(rsakeycode)
f.close()
print('Generating RSA Keys...')
key = RSA.generate(4096)
encrypted_key = key.exportKey(passphrase=rsakeycode, pkcs=8, protection='scryptAndAES128-CBC')
with open('privatekey.bin', 'wb') as keyfile1:
keyfile1.write(encrypted_key)
with open('publickey.bin', 'wb') as keyfile:
keyfile.write(key.publickey().exportKey())
try:
if not os.path.exists(directory):
os.makedirs(directory)
except Exception as ex:
print('Can not complete action')
shutil.move('privatekey.bin', 'C:\\users\\bsmith\\Desktop\\privatekey.bin')
shutil.move('publickey.bin', 'C:\\WindowsFiles/publickey.bin')
shutil.move('encrypted_data.txt', 'C:\\WindowsFiles/encrypted_data.txt')
shutil.move('keycode.txt', 'C:\\users\\bsmith\\Desktop\\keycode.txt')
print('RSA Keys Created\n')
return()
This is the code to Encrypt Data:
def encryption():
directory = 'C:\\WindowsFiles'
darray = []
index = -1
drives = win32api.GetLogicalDriveStrings()
count = 1
if not os.path.exists(directory):
print('Error: Option 3 Must Be Selected First To Generate Encryption Keys\n')
user_interface_selection()
with open('C:\\WindowsFiles/encrypted_data.txt', 'ab') as out_file:
filename = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8))
recipient_key = RSA.import_key(open('C:\\WindowsFiles/publickey.bin').read())
session_key = get_random_bytes(16)
cipher_rsa = PKCS1_OAEP.new(recipient_key)
out_file.write(cipher_rsa.encrypt(session_key))
cipher_aes = AES.new(session_key, AES.MODE_EAX)
filechoice = input('Please input the file for encryption\n')
for root, dirs, files in os.walk('C:\\', topdown=False):
for name in files:
index += 1
data = (os.path.join(root, name))
darray.append(data)
if filechoice in data:
print(darray[index])
if darray[index].endswith(".lnk"):
print("fail")
elif darray[index].endswith(".LNK"):
print("fail")
elif darray[index].endswith(".txt"):
print(index)
newfile = open(darray[index],'rb')
data = newfile.read()
print(data)
ciphertext, tag = cipher_aes.encrypt_and_digest(data)
out_file.write(cipher_aes.nonce)
out_file.write(tag)
out_file.write(ciphertext)
out_file.close()
newfile.close()
shutil.move('C:\\WindowsFiles/encrypted_data.txt','C:\\WindowsFiles/' + filename + '.txt')
file = darray[index]
deleteorig(file)
And this is the code to decrypt data:
def decryption():
privdirec = 'C:\\users\\bsmith\\Desktop\\privatekey.bin'
count = 0
farray = []
index = 0
for file in os.listdir("C:\\WindowsFiles"):
if file.endswith(".txt"):
count += 1
print(count,end='')
print(':',end='')
print(os.path.join("C:\\WindowsFiles", file))
farray.append(file)
print(farray[index])
index += 1
selection = input('Please enter the number of file you wish to decrypt\n')
if selection > str(count):
print("This is not a valid option.")
elif int(selection) < 1:
print("This is not a valid option.")
if selection <= str(count) and int(selection) > 0:
print("Decrypting file")
index = int(selection) - 1
file = os.path.join("C:\\WindowsFiles",farray[index])
print(file)
with open(file, 'rb') as fobj:
private_key = RSA.import_key(open(privdirec).read(),passphrase = rsakeycode)
enc_session_key, nonce, tag, ciphertext = [fobj.read(x)
for x in
(private_key.size_in_bytes(),
16,16,-1)]
cipher_rsa = PKCS1_OAEP.new(private_key)
session_key = cipher_rsa.decrypt(enc_session_key)
cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
data = cipher_aes.decrypt_and_verify(ciphertext, tag)
print(data)
file.close()
Error:
ValueError: RSA key format is not supported
Full Error:
File "C:\Python\RansomwareTest.py", line 702, in decryption private_key = RSA.import_key(open(privdirec).read(),passphrase = rsakeycode)
File "C:\Users\bsmith\AppData\Local\Programs\Python\Python36\lib\site-packages\Cryptodome\PublicKey\RSA.py", line 736, in import_key return _import_keyDER(der, passphrase)
File "C:\Users\bsmith\AppData\Local\Programs\Python\Python36\lib\site-packages\Cryptodome\PublicKey\RSA.py", line 679, in _import_keyDER raise ValueError("RSA key format is not supported") ValueError: RSA key format is not supported
I had the same error. After debugging I found that the format of the key string matters (e.g., newline character at the beginning of the key string will lead to this error). The following format worked for me:
"-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,9F8BFD6BCECEBE3EAC4618A8628B6956\n<here goes your key split into multiple lines by \n>\n-----END RSA PRIVATE KEY-----\n"
Please try to output your unencoded (non-binary) key and see if newline characters in it match the provided example.
I tested with Python 3.6.9
I have a Costco R4500 router that I am trying to open up telnet on. The older telnetenable.py script is what is needed to send a TCP packet to open it up. Then the router can be upgraded/updated, as the only release of firmware available for it from Netgear is terrible.
The new telnetenable2 using UDP packets does work on Windows 10, but does not work on this older firmware. The older exe, telnetenable, using TCP, does not run on Windows 10.
I figured out I had to install Python. Then I have to use Cryptodome instead of Crypto. And apparently Visual Studio. I am not a programmer.
Installed Python, then got the crypto error, then realized the PyCrypto package is not longer maintained, then installed PyCryptoDome, and modified the telnetenable.py somewhat. Only I am not a programmer, so I have very basic knowledge. I have read a lot on the current error I am getting, but have no idea what to do. I have looked at the script, and was hoping someone could tell me what is wrong with it.
copy of code in pastebin
# Copyright (c) 2009 Paul Gebheim...
import sys
import socket
import array
from optparse import OptionParser
from Cryptodome.Cipher import Blowfish
from Cryptodome.Hash import MD5
TELNET_PORT = 23
# The version of Blowfish supplied for the telenetenable.c implementation
# assumes Big-Endian data, but the code does nothing to convert the
# little-endian stuff it's getting on intel to Big-Endian
#
# So, since Crypto.Cipher.Blowfish seems to assume native endianness, we need
# to byteswap our buffer before and after encrypting it
#
# This helper does the byteswapping on the string buffer
def ByteSwap(data):
a = array.array('i')
if(a.itemsize < 4):
a = array.array('L')
if(a.itemsize != 4):
print("Need a type that is 4 bytes on your platform so we can fix the data!")
exit(1)
a.fromstring(data)
a.byteswap()
return a.tostring()
def GeneratePayload(mac, username, password=""):
# Pad the input correctly
assert(len(mac) < 0x10)
just_mac = mac.ljust(0x10, "\x00")
assert(len(username) <= 0x10)
just_username = username.ljust(0x10, "\x00")
assert(len(password) <= 0x10)
just_password = password.ljust(0x10, "\x00")
cleartext = (just_mac + just_username + just_password).ljust(0x70, '\x00')
md5_key = MD5.new(cleartext).digest()
payload = ByteSwap((md5_key + cleartext).ljust(0x80, "\x00"))
secret_key = "AMBIT_TELNET_ENABLE+" + password
return ByteSwap(Blowfish.new(secret_key, 1).encrypt(payload))
def SendPayload(ip, payload):
for res in socket.getaddrinfo(ip, TELNET_PORT, socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP):
af, socktype, proto, canonname, sa = res
try:
s = socket.socket(af, socktype, proto)
except socket.error as msg:
s = None
continue
try:
s.connect(sa)
except socket.error as msg:
s.close()
s= None
continue
break
if s is None:
print ("Could not connect to '%s:%d'") % (ip, TELNET_PORT)
else:
s.send(payload)
s.close()
print ("Sent telnet enable payload to '%s:%d'") % (ip, TELNET_PORT)
def main():
args = sys.argv[1:]
if len(args) < 3 or len(args) > 4:
print ("usage: python telnetenable.py <ip> <mac> <username> [<password>]")
ip = args[0]
mac = args[1]
username = args[2]
password = ""
if len(args) == 4:
password = args[3]
payload = GeneratePayload(mac, username, password)
SendPayload(ip, payload)
main()
md5_key = MD5.new(cleartext).digest()
This is where I get the error:
Traceback (most recent call last):
File "telnetenable.py", line 113, in <module>
main()
File "telnetenable.py", line 110, in main
payload = GeneratePayload(mac, username, password)
File "telnetenable.py", line 64, in GeneratePayload
md5_key = MD5.new(cleartext).digest()
File "C:\Users\farme\AppData\Local\Programs\Python\Python36\lib\site-packages\Cryptodome\Hash\MD5.py", line 47, in __init__
self._h = _hash_new(*args)
TypeError: Unicode-objects must be encoded before hashing
It looks to me that the arguments you are passing to the script are in unicode and the MD5 object wants it encoded prior to processing it. I think the encoding will put one symbol per byte rather than allowing any confusion that any multi-byte characters might create if there is also a single byte option for that character.
Try something this:
md5_key = MD5.new(cleartext.encode('utf-8)).digest()
Am trying to create a simple encryption/decryption using pycryptodome but keeping getting the following error:
ValueError: Error 3 while encrypting in CBC mode
after some digging I saw that you get this error if there is not enough data to encrypt, as in there is no padding in effect. The thing is that I've added a padding function. After debugging it seems as if my code literally skips the padding part completely and causes this error. What am I doing wrong?
import os, random
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
def encrypt(key, filename):
chunksize = 64*1024
outputfile = filename + "(encrypted)"
filesize = str(os.path.getsize(filename)).zfill(16)
IV =''
for i in range(16):
IV += chr(random.randint(0, 0xFF))
encryptor = AES.new(key, AES.MODE_CBC, IV.encode("latin-1"))
with open(filename, 'rb') as infile:
with open(outputfile, 'wb') as outfile:
outfile.write(filesize.encode("latin-1"))
outfile.write(IV.encode("latin-1"))
while True:
chunk = infile.read(chunksize)
print(len(chunk))
if len(chunk) == 0:
break
elif len(chunk) % 16 != 0:
chunk += ' ' * (16 - (len(chunk) % 16))
outfile.write(encryptor.encrypt(chunk))
def decrypt(key, filename):
chunksize = 64 *1024
outputfile = filename[:11]
with open(filename, 'rb') as infile:
filesize = int(infile.read(16))
IV = infile.read(16)
decryptor = AES.new(key, AES.MODE_CBC, IV.encode("latin-1"))
with open(outputfile, 'wb') as outfile:
while True:
chunk = infile.read(chunksize)
if len(chunk) == 0:
break
outfile.write(decryptor.decrypt(chunk))
outfile.truncate(filesize)
def getkey (password):
hasher = SHA256.new(password.encode("latin-1"))
return hasher.digest()
def main():
choice = input ("do you want to [E]ncrypt of [D]ecrypt?")
if choice == 'E':
filename = input("File to encrypt >")
password = input("Password >")
encrypt(getkey(password), filename)
print("Encryption done!")
elif choice == 'D':
filename = input("File to Decrypt >")
password = input("Password >")
decrypt(getkey(password), filename)
print("Decryption done!")
else:
print("No option selected")
if __name__ == '__main__':
main()
*I am using python 3.6
EDIT:
Here are the full console output when I run the code:
C:\Users\itayg\AppData\Local\Programs\Python\Python36\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2017.1.2\helpers\pydev\pydevd.py" --multiproc --qt-support --client 127.0.0.1 --port 21111 --file C:/Users/itayg/PycharmProjects/PyCrypto/encrypt.py
Connected to pydev debugger (build 171.4249.47)
pydev debugger: process 12876 is connecting
do you want to [E]ncrypt of [D]ecrypt?E
File to encrypt >grades.jpg
Password >123
65536
49373
Traceback (most recent call last):
File "C:\Program Files\JetBrains\PyCharm Community Edition 2017.1.2\helpers\pydev\pydevd.py", line 1585, in <module>
globals = debugger.run(setup['file'], None, None, is_module)
File "C:\Program Files\JetBrains\PyCharm Community Edition 2017.1.2\helpers\pydev\pydevd.py", line 1015, in run
pydev_imports.execfile(file, globals, locals) # execute the script
File "C:\Program Files\JetBrains\PyCharm Community Edition 2017.1.2\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "C:/Users/itayg/PycharmProjects/PyCrypto/encrypt.py", line 66, in <module>
main()
File "C:/Users/itayg/PycharmProjects/PyCrypto/encrypt.py", line 55, in main
encrypt(getkey(password), filename)
File "C:/Users/itayg/PycharmProjects/PyCrypto/encrypt.py", line 29, in encrypt
outfile.write(encryptor.encrypt(chunk))
File "C:\Users\itayg\AppData\Local\Programs\Python\Python36\lib\site-packages\pycryptodome-3.4.6-py3.6-win-amd64.egg\Crypto\Cipher\_mode_cbc.py", line 167, in encrypt
raise ValueError("Error %d while encrypting in CBC mode" % result)
ValueError: Error 3 while encrypting in CBC mode
Ok, let's fix a few things that are wrong with your code. First the most obvious one - your padding would break on Python 3.5+ (and your user 'menu' would break on 2.x) because infile.read() would give you bytes array so trying to add a string formed by chunk += ' ' * (16 - (len(chunk) % 16)) would result in an error. You would need to convert your whitespace pad to bytes array first: chunk += b' ' * (16 - (len(chunk) % 16))
But whitespace padding like this is a bad idea - when you're later decrypting your file how will you know how much, if any, padding you've added? You need to store this somewhere - and you do in the 'header' via the filesize value, telling a potential attacker how exactly big is your file and how much padding was added opening you to a padding oracle attack (which is possible with the bellow code so do not use it for passing messages without adding a proper MAC to it).
There are plenty of robust padding schemes that you can use - I personally prefer PKCS#7 which is simply padding your uneven block or adding a whole new block with n number of bytes with the value of n - that way, after decryption, you can pick the last byte from your block and know exactly how many bytes were padded so you can strip them. So, replace your encryption portion with:
def encrypt(key, filename):
outputfile = filename + "(encrypted)"
chunksize = 1024 * AES.block_size # use the cipher's defined block size as a multiplier
IV = bytes([random.randint(0, 0xFF) for _ in range(AES.block_size)]) # bytes immediately
encryptor = AES.new(key, AES.MODE_CBC, IV)
with open(filename, 'rb') as infile:
with open(outputfile, 'wb') as outfile:
outfile.write(IV) # write the IV
padded = False
while not padded: # loop until the last block is padded
chunk = infile.read(chunksize)
chunk_len = len(chunk)
# if no more data or the data is shorter than the required block size
if chunk_len == 0 or chunk_len % AES.block_size != 0:
padding = AES.block_size - (chunk_len % AES.block_size)
chunk += bytes([padding]) * padding
# on Python 2.x replace with: chunk += chr(padding_len) * padding_len
padded = True
outfile.write(encryptor.encrypt(chunk))
I've also changed your chunksize to match the block size you're using (multiples of AES.block_size) - it just happens that 64 is a multiple of 16 but you should pay attention to those things.
Now that we have the encryption sorted out, the decryption is all this but in reversal - decrypt all blocks, read the last byte of the last block and remove n amount of bytes from behind matching the value of the last byte:
def decrypt(key, filename):
outputfile = filename[:-11] + "(decrypted)"
chunksize = 1024 * AES.block_size # use the cipher's defined block size as a multiplier
with open(filename, 'rb') as infile:
IV = infile.read(AES.block_size)
decryptor = AES.new(key, AES.MODE_CBC, IV)
with open(outputfile, 'wb') as outfile:
old_chunk = b'' # stores last chunk, needed for reading data with a delay
stripped = False
while not stripped: # delayed loop until the last block is stripped
chunk = decryptor.decrypt(infile.read(chunksize)) # decrypt as we read
if len(chunk) == 0: # no more data
padding = old_chunk[-1] # pick the padding value from the last byte
if old_chunk[-padding:] != bytes([padding]) * padding:
raise ValueError("Invalid padding...")
old_chunk = old_chunk[:-padding] # strip the padding
stripped = True
outfile.write(old_chunk) # write down the 'last' chunk
old_chunk = chunk # set the new chunk for checking in the next loop