Simple /etc/shadow Cracker - linux

I'm trying to get this shadow file cracker working but I keep getting a TypeError: integer required.
I'm sure its the way I'm using the bytearray function. I've tried creating a new object with bytearray for the "word" and the "salt" however to no avail. So then I tried passing the bytearray constructor to the pbkdf2 function and still nothing. I will post the code:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import hashlib, binascii
import os,sys
import crypt
import codecs
from datetime import datetime,timedelta
import argparse
today = datetime.today()
# Takes in user and the encrypted passwords and does a simple
# Brute Force Attack useing the '==' operator. SHA* is defined by
# a number b/w $, the char's b/w the next $ marker would be the
# rounds, then the salt, and after that the hashed password.
# object.split("some symbol or char")[#], where # is the
# location/index within the list
def testPass(cryptPass,user):
digest = hashlib.sha512
dicFile = open ('Dictionary.txt','r')
ctype = cryptPass.split("$")[1]
if ctype == '6':
print "[+] Hash type SHA-512 detected ..."
print "[+] Be patien ..."
rounds = cryptPass.split("$")[2].strip('rounds=')
salt = cryptPass.split("$")[3]
print "[DEBUG]: " + rounds
print "[DEBUG]: " + salt
# insalt = "$" + ctype + "$" + salt + "$" << COMMENTED THIS OUT
for word in dicFile.readlines():
word = word.strip('\n')
print "[DEBUG]: " + word
cryptWord = hashlib.pbkdf2_hmac(digest().name,bytearray(word, 'utf-8'),bytearray(salt, 'utf-8'), rounds)
if (cryptWord == cryptPass):
time = time = str(datetime.today() - today)
print "[+] Found password for the user: " + user + " ====> " + word + " Time: "+time+"\n"
return
else:
print "Nothing found, bye!!"
exit
# argparse is used in main to parse arguments pass by the user.
# Path to shadow file is required as a argument.
def main():
parse = argparse.ArgumentParser(description='A simple brute force /etc/shadow .')
parse.add_argument('-f', action='store', dest='path', help='Path to shadow file, example: \'/etc/shadow\'')
argus=parse.parse_args()
if argus.path == None:
parse.print_help()
exit
else:
passFile = open (argus.path,'r', 1) # ADDING A 1 INDICATES A BUFFER OF A
for line in passFile.readlines(): # SINGLE LINE '1<=INDICATES
line = line.replace("\n","").split(":") # EXACT BUFFER SIZE
if not line[1] in [ 'x', '*','!' ]:
user = line[0]
cryptPass = line[1]
testPass(cryptPass,user)
if __name__=="__main__":
main()
OUTPUT:
[+] Hash type SHA-512 detected ...
[+] Be patien ...
[DEBUG]: 65536
[DEBUG]: A9UiC2ng
[DEBUG]: hellocat
Traceback (most recent call last):
File "ShadowFileCracker.py", line 63, in <module>
main()
File "ShadowFileCracker.py", line 60, in main
testPass(cryptPass,user)
File "ShadowFileCracker.py", line 34, in testPass
cryptWord = hashlib.pbkdf2_hmac(digest().name,bytearray(word, 'utf-8'),bytearray(salt, 'utf-8'), rounds)
TypeError: an integer is required

The rounds variable needs to be an integer, not a string. The correct line should be:
rounds = int(cryptPass.split("$")[2].strip('rounds='))
Also, strip() might not be the best method for removing the leading "rounds=". It will work, but it strips a set of characters and not a string. A slightly better method would be:
rounds = int(cryptPass.split("$")[2].split("=")[1])

Related

I have a python script that works perfectly in the Thonny IDE, but fails in terminal

Firstly, here's the code:
#!/usr/bin/python3
import re, pexpect, os
file = '/home/homebridge/flags/Restart.flag'
f = open(file, 'w')
f.close()
os.system("sudo systemctl stop homebridge")
os.system("sudo chmod -R a+rwx /var/lib/homebridge")
child = pexpect.spawn('tuya-cli wizard')
child.expect('\r\n')
child.sendline('y')
child.expect('\r\n')
child.sendline('XXXXXXXXXXXXXXXX')
data = child.read()
data = data.decode("utf-8")
devices = data.split('},')
devicesO = []
class device:
name = ""
ID = ""
key = ""
def __init__(self, name, ID, key):
self.name = name
self.ID = ID
self.key = key
def __lt__(self, other):
return self.name < other.name
for i in devices:
n = re.search("name: \'(.*)\'", str(i)).group(1)
I = re.search("id: \'(.*)\'", str(i)).group(1)
k = re.search("key: \'(.*)\'", str(i)).group(1)
if n != ("Clock"):
devicesO.append(device(n, I, k))
entries = []
devicesO.sort()
for device in devicesO:
if "phone charger" not in device.name:
s1 = "{\n\"name\": \"" + device.name + "\",\n\"id\": \"" + device.ID + "\",\n\"key\": \"" + device.key + "\","
s2 = """
"type": "RGBTWLight",
"manufacturer": "SmartLife",
"model": "Light",
"dpPower": "20",
"dpMode": "21",
"dpBrightness": "22",
"dpColorTemperature": "23",
"dpColor": "24",
"colorFunction": "HSB",
"scaleBrightness": 1000
}"""
else:
s1 = "{\n\"name\": \"" + device.name + "\",\n\"id\": \"" + device.ID + "\",\n\"key\": \"" + device.key + "\","
s2 = """
"type": "Outlet",
"manufacturer": "SmartLife",
"model": "Outlet",
"dpPower": "1"
}"""
entries.append(s1 + s2)
string = ",\n".join([str(entry) for entry in entries])
config = open('/var/lib/homebridge/config.json', 'r+')
x = config.read()
config.close()
#print(x)
x = re.sub("\"TuyaLan\",\n.*\"devices\": \[((.|\n)*?)\]", "\"TuyaLan\",\n\"devices\": [\n" + string + "\n]", x)
#print(x)
#x = re.sub("\"TuyaLan\",\n.*\"devices\": \[((.|\n)*?)\]", "\"TuyaLan\",\n.*\"devices\": [\nTEST\n]", x)
config = open('/var/lib/homebridge/config.json', 'w+')
config.write(x)
config.close()
config = open('/var/lib/homebridge/config.json', 'r+')
print (config.read())
config.close()
os.remove(file)
os.system("sudo systemctl restart homebridge")
This executes as expected in the IDE, stopping the homebridge service, pulling relevant data from the tuya-cli utility, regex and text replacement, all of it. However, when I try and run it in the terminal without sudo, the first regex search returns an empty object and the script fails. When I run it with sudo, it stalls for a while then times out on the pexpect step at the beginning. I've researched before posting, but I have no clue how to solve this one. It doesn't appear to be a path issue, I used pip3 to install both re and pexpect, and os is obviously packaged with the raspbian install. Any clues would be great.
Error without sudo:
pi#raspberrypi:~ $ /home/homebridge/scripts/updateConfig.py
Traceback (most recent call last):
File "/home/homebridge/scripts/updateConfig.py", line 34, in <module>
n = re.search("name: \'(.*)\'", str(i)).group(1)
AttributeError: 'NoneType' object has no attribute 'group'
With sudo:
pi#raspberrypi:~ $ sudo /home/homebridge/scripts/updateConfig.py
Traceback (most recent call last):
File "/home/homebridge/scripts/updateConfig.py", line 10, in <module>
child.expect('\r\n')
File "/usr/local/lib/python3.7/dist-packages/pexpect/spawnbase.py", line 344, in expect
timeout, searchwindowsize, async_)
File "/usr/local/lib/python3.7/dist-packages/pexpect/spawnbase.py", line 372, in expect_list
return exp.expect_loop(timeout)
File "/usr/local/lib/python3.7/dist-packages/pexpect/expect.py", line 181, in expect_loop
return self.timeout(e)
File "/usr/local/lib/python3.7/dist-packages/pexpect/expect.py", line 144, in timeout
raise exc
pexpect.exceptions.TIMEOUT: Timeout exceeded.
<pexpect.pty_spawn.spawn object at 0x766c4510>
command: /usr/bin/tuya-cli
args: ['/usr/bin/tuya-cli', 'wizard']
buffer (last 100 chars): b'\x1b[32m?\x1b[39m \x1b[1mThe API key from tuya.com:\x1b[22m\x1b[0m \x1b[0m\x1b[29D\x1b[29C'
before (last 100 chars): b'\x1b[32m?\x1b[39m \x1b[1mThe API key from tuya.com:\x1b[22m\x1b[0m \x1b[0m\x1b[29D\x1b[29C'
after: <class 'pexpect.exceptions.TIMEOUT'>
match: None
match_index: None
exitstatus: None
flag_eof: False
pid: 1470
child_fd: 5
closed: False
timeout: 30
delimiter: <class 'pexpect.exceptions.EOF'>
logfile: None
logfile_read: None
logfile_send: None
maxread: 2000
ignorecase: False
searchwindowsize: None
delaybeforesend: 0.05
delayafterclose: 0.1
delayafterterminate: 0.1
searcher: searcher_re:
0: re.compile(b'\r\n')
Possible short answer: Your IDE is probably automatically adding carriage returns with your sendlines, which is why your code runs in the IDE, but not at the terminal. Sendline adds a line feed (\n), but not a carriage return (\r). You should add a \r after each sendline (e.g., child.sendline('XXXXXXXXXXXXXXXX\r')) to complete the CRLF (\r\n).
Long explanation:
Based on your code, when you spawned the child, you expected a CRLF. However, pexpect searches are not greedy and will stop at the first CRLF they encounter. Unfortunately, when I tested your code, pexpect stopped at the CRLF after the command you entered, not the prompt afterwards:
child = pexpect.spawn('tuya-cli wizard')
child.expect('\r\n')
print(child.before)
Output
b" tuya-cli wizard"
You should be looking for a prompt or a message instead, such as The API key from tuya.com: or Password::
# tuya-cli wizard
The API key from tuya.com:
The API secret from tuya.com:
Provide a 'virtual ID' of a device currently registered in the app:
or
# sudo tuya-cli wizard
Password:
However, I think both of your errors occurred because you did not include a carriage return (\r) with your sendlines. The first error occurred because, at the prompt, The API key from tuya.com:, you sent 'y', not 'y\r', so nothing was entered at the prompt. You then searched for CRLF, but since you had not included a \r, pexpect found the original CRLF after b" tuya-cli wizard".
The expect call actually caused a carriage return, but, unfortunately, your code was now a step behind, and was interacting with the previous prompt, not the current prompt. That is why data = child.read() ended up reading the wrong output, resulting in a NoneType object.
The second error occurred because the pexpect cursor moved up to the The API key from tuya.com: prompt, looking for a CRLF. Since it is a prompt, it does not end with a CRLF, so the pexpect search timed out (those \x1b escape sequences are just for formatting and color):
pexpect.exceptions.TIMEOUT: Timeout exceeded.
args: ['/usr/bin/tuya-cli', 'wizard']
before (last 100 chars): b'\x1b[32m?\x1b[39m \x1b[1mThe API key from tuya.com:\x1b[22m\x1b[0m \x1b[0m\x1b[29D\x1b[29C'
searcher: searcher_re:
0: re.compile(b'\r\n')
Note that there is no \r\n in the buffer or before byte strings.
I would do something like:
...
while True:
index = child.expect(
["Password:", "The API key from tuya.com:", pexpect.TIMEOUT, pexpect.EOF, ])
if index == 0:
password = getpass() # You will need 'from getpass import getpass'
child.sendline(password) # CR's are usually not needed with variables
elif index == 1:
# This is what you want
child.sendline("XXXXXXXXXXXXXXXX\r")
break
elif index == 2:
raise RuntimeError("Search string not found.")
elif index ==3:
raise RuntimeError("Child closed.")
child.expect("The API secret from tuya.com:")
child.sendline("XXXXXXXXXXXXXXXX\r")
...
Good luck with your code!

Python3: how to use module 'Import click' & Parsing command line

I am a hobby radio amateur [G6SGA] not a programmer but I do try. :)
using python3. I am trying to do the following and really can't get my head around - argparse and ended up trying to use 'Import click'. Still can't get my head around so here I am. Any all (polite) :) suggestions welcome.
I wish to ---
cmd line> python3 scratch.py [no options supplied]
output> "Your defaults were used and are:9600 and '/dev/ttyAMA0' "
or
cmd line> python3 scratch.py 115200 '/dev/ttyABC123'
output> "Your input values were used and are: 115200 and '/dev/ttyAMA0'"
so a command line that will take [or NOT] argument/s. store the argument to a variable in the code for future use.
This some of what I have tried: Yes I accept it's a mess
#!/usr/bin/env python3
# -*- coding: utf_8 -*-
# ========================
# Include standard modules
# import click
# baud default = 9600
# port default = "/dev/ttyAMA0"
import click
#click.command()
# #click.option('--baud', required = False, default = 9600, help = 'baud rate defaults to: 9600')
# #click.option('--port', required = False, default = '/dev/ttyAMA0', help = 'the port to use defaults to: /dev/ttyAMA0')
#click.option('--item', type=(str, int))
def putitem(item):
click.echo('name=%s id=%d' % item)
def communications():
""" This checks the baud rate and port to use
either the command line supplied item or items.
Or uses the default values
abaud = 9600 # default baud rate
b=abaud
aport = "/dev/ttyAMA0"
p=aport
print(f"abaud = {b} and aport = {p}")
"""
# now I wish to check if there were supplied values
# on the command line
# print(f"Baud supplied {click.option.} port supplied {port}" )
if __name__ == '__main__':
putitem() # communications()
The code I have used to work this all out is below, I hope it helps somebody. Any better ways or mistakes please advise.
#!/usr/bin/env python3
# -*- coding: utf_8 -*-
import click
from typing import Tuple
# Command Line test string: python scratch_2.py -u bbc.co.uk aaa 9600 bbb ccc
myuri = "" # This is a placeholder for a GLOBAL variable -- take care!
list1 = [] # This is a placeholder for a GLOBAL variable -- take care!
#click.command(name="myLauncher", context_settings={"ignore_unknown_options": True})
#click.option('--uri', '-u', type=click.STRING, default=False, help ="URI for the server")
#click.argument('unprocessed_args', nargs = -1, type = click.UNPROCESSED)
def main(uri: str, unprocessed_args: Tuple[str, ...]) -> None:
# ==================== Checking the command line structure and obtaining variables
global myuri # define the use of a GLOBAL variable in this function
temp = list((str(j) for i in {unprocessed_args: Tuple} for j in i)) # sort out the command line arguments
res = len(temp)
# printing result
print("")
for e in range(0 ,res): # list each of the command line elements not including any uri
print("First check: An input line Tuple element number: " + str(e) +": " + str(temp[e])) # elements base 0
# ==================== deal with any URI supplied -- or NOT
if uri is False: #if --uri or -u is not supplied
print("No uri supplied\n")
print("The input line tuple list elements count: " + str(res))
# set a defaul GLOBAL value of myuri if it is not supplied
myuri = "https://192.168.0.90:6691/" #
else:
print("\nThe input line tuple list elements count : " + str(res) + " and we got a uri")
myuri = uri # set the GLOBAL value of myuri if the uri is
print(f"which is: {uri}, and therefore myuri also is: {myuri}") # a temp print to prove the values of the GLOBAL variable 'myuri'
# ==============================================================================================
# Testing choice of baud rate on command line
db_list = {
'4800': 'TEST48',
'9600': 'TEST96',
'19200': 'TEST19',
'38400': 'TEST38',
'57600': 'TEST57',
'115200': 'TEST11',
}
# Print databases ----- db_list ----- listed in dictionary
print("\nDatabases:")
for e in range(0 ,res) :
""" list each of the command line elements not including any uri
print("Second Check: An input line Tuple element number: " + str(e) +": " + str(temp[e]))
elements base 0 """
if str(temp[e]) in db_list.keys() :
print(f"The index of db contains {str(temp[e])}, The index refers to: {db_list[str(temp[e])]}")
if __name__ == "__main__":
# pylint: disable=no-value-for-parameter, unexpected-keyword-arg
main()

Converts strings of binary to binary

I have a text file and a would like to read it in binary so I can transform its content into hexadecimal characters.
Then, I need to replace '20' by '0' and '80', 'e2', '8f' by '1'.
This would create a string of 0 and 1 (basically binary).
Finally, I need to convert this binary string into ascii characters.
I'm almost finish but I struggle with the last part:
import binascii
import sys
bin_file = 'TheMessage.txt'
with open(bin_file, 'rb') as file:
file_content = file.read().hex()
file_content = file_content.replace('20', '0').replace('80', '1').replace('e2', '1').replace('8f', '1')
print(file_content)
text_bin = binascii.a2b_uu(file_content)
The last line produces an error (I do not fully understand strings/hex/binary interpretation in python):
Traceback (most recent call last):
File "binary_to_string.py", line 34, in <module>
text_bin = binascii.a2b_uu(file_content)
binascii.Error: Trailing garbage
Could you give me a hand?
I'm working on this file: blank_file
I think you're looking for something like this? Refer to comments for why I do what I did.
import binascii
import sys
bin_file = 'TheMessage.txt'
with open(bin_file, 'rb') as file:
file_content = file.read().hex()
file_content = file_content.replace('20', '0').replace('80', '1').replace('e2', '1').replace('8f', '1')
# First we must split the string into a list so we can get bytes easier.
bin_list = []
for i in range(0, len(file_content), 8): # 8 bits in a byte!
bin_list.append(file_content[i:i+8])
message = ""
for binary_value in bin_list:
binary_integer = int(binary_value, 2) # Convert the binary value to base2
ascii_character = chr(binary_integer) # Convert integer to ascii value
message+=ascii_character
print(message)
One thing I noticed while working with this is that using your solution/file, there are 2620 bits, and this does not divide into 8, so it can not properly become bytes.

Input between multiline strings python3

Well, you see, I have searched and searched without any results on how to make an input between multiline strings, such as something like this:
print("******************************\nPassword: ", end="")
x = input()
print("******************************")
print("Your password is %s" % (x))
( but when i do this, the last of asterisks line is not printed until the input has be done )
or also like in GNU nano, when you write text you do it between strings with new lines
Thank you very much in advance
I get it, you have to do a return (\r) and then a backspace (\b), here is my program that prompts for the password and a function to make it easier.
Password prompt program:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from readchar import readchar
from os import get_terminal_size
from sys import stdout
print("\n"*((get_terminal_size()[1]-5)//2))
spaces = " " * ((get_terminal_size()[0]-53)//2)
passwd = ""
text = ""
text+="\r\b\r\b\r\b "
text+="""{0}#####################################################\n"""
text+="""{0}# #\n"""
text+="""{0}# Password: #\n"""
text+="""{0}# #\n"""
text+="""{0}#####################################################\n"""
text+="""\r\b\r\b\r\b\r"""
text+="""{0}# Password: {1}"""
stdout.write(text.format(spaces, "*" * len(passwd)))
stdout.flush()
try:
ch = readchar()
while ch != "\n":
if ch == "\x7f":
passwd = passwd[:-1]
elif ch in ["\r", "\x03"]:
break
else:
passwd += ch
if len(passwd) > 32:
stdout.write(text % ("*"*32))
else:
stdout.write(text.format(spaces, "*" * len(passwd)))
stdout.flush()
ch = readchar()
except(KeyboardInterrupt):
pass
print("\n"*5+passwd )
Function:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def backspace_enter(charlen, get_back=False):
print("\r\b"*charlen, end="")
if get_back == True:
print("\n"*charlen, end="")
I think that you are dealing with buffering issues on stdout. Add flush =true to your print statement.
print("Hello\n", flush=True);

Skipping elif statement?

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

Resources