1

I have an encrypted ZIP file and for some reason, any password I feed it doesn't seem to matter as it can add files to the archive regardless. I checked for any ignored exceptions or anything, but nothing seems to be fairly obvious.

I posted the minimalist code below:

import zipfile

z = zipfile.ZipFile('test.zip', 'a') #Set zipfile object
zipPass = str(input("Please enter the zip password: "))
zipPass = bytes(zipPass, encoding='utf-8')
z.setpassword(zipPass) #Set password
z.write("test.txt")

I am not sure what I am missing here, but I was looking around for anything in zipfile that can handle encrypted zipfiles and add files into them using the password, as the only thing I have is the ``z.setpassword()` function that seems to not work here.

TL;DR: z.write() doesn't throw an exception and neither does z.setpassword() or anything zipfile related when fed the incorrect password, and willingly adds files no matter what. I was expecting to get BadPasswordForFile.

Is there any way to do this?

Dharman
  • 30,962
  • 25
  • 85
  • 135
Vasilisa
  • 131
  • 1
  • 10
  • Can you try to make a minimal reproducible example? This is a little hard to follow. Also. I'm not sure why you are doing imports all over the place. Stick to imports at the top of your code, there's no reason to import at arbitrary locations in the code. – Tom Myddeltyn Dec 05 '20 at 21:52
  • From the documentation. When you do the `setpassword(pwd)` function, it sets the password to be used by default with other commands, so maybe this is working for you or are you expecting a different result? – Tom Myddeltyn Dec 05 '20 at 21:53
  • @TomMyddeltyn I was afraid it was a little hard to follow. I am expecting the GUI to display a label with an error message if the user enters an incorrect password, but the ZIP file seems to be able to edit an encrypted ZIP no matter what password the user enters. – Vasilisa Dec 05 '20 at 22:03
  • @TomMyddeltyn I updated the post with minimalist code. I am not sure if I am just being stupid, but I am expecting BadPasswordForFile error, not for it to just... work. – Vasilisa Dec 05 '20 at 22:13
  • What happens when you open the zip file with a proven zip application? I think, maybe that the password is only for unzip and adding arbitrary files isn't an issue? From here https://security.stackexchange.com/questions/33081/zip-file-with-two-password I think you can have different passwords on different files in zip. I think the key will be what happens when you try to unzip. – Tom Myddeltyn Dec 05 '20 at 22:52
  • @TomMyddeltyn I opened it with Ubuntu Archive Manager and it opens just fine, and the original file in there has a padlock which means I cannot open or touch it at all. The file I added with the above code has no padlock at all, and I can do as I wish with it. It's almost like the files are the ones encrypted and password protected and not the archive... – Vasilisa Dec 06 '20 at 00:15
  • 1
    If I'm remembering correctly, passwords are set for specific files *within* a zip archive. – Maarten Bodewes Dec 06 '20 at 01:25
  • @MaartenBodewes Thank you, I'll see if I can find a workaround. – Vasilisa Dec 06 '20 at 14:32

2 Answers2

2

What I found in the documentation for zipfile is that the library supports decryption only with a password. It cannot encrypt. So you won't be able to add files with a password.

It supports decryption of encrypted files in ZIP archives, but it currently cannot create an encrypted file. https://docs.python.org/3/library/zipfile.html

EDIT: Further, looking into python bugs Issue 34546: Add encryption support to zipfile it appears that in order to not perpetuate a weak password scheme that is used in zip, they opted to not include it.

Something that you could do is utilize subprocess to add files with a password. Further, if you wanted to "validate" the entered password first, you could do something like this but you'd have to know the contents of the file because decrypt will happily decrypt any file with any password, the plaintext result will just be not correct.

Issues you'll have to solved:

  • Comparing file contents to validate password
  • Handling when a file exists already in the zip file
  • handling when the zipfile already exists AND when it doesn't.
import subprocess
import zipfile

def zip_file(zipFilename, filename):
    zipPass = str(input("Please enter the zip password: "))
    zipPass = bytes(zipPass, encoding='utf-8')

    #If there is a file that we know the plain-text (or original binary)
    #TODO: handle fipFilename not existing.
    validPass=False
    with zipfile.ZipFile(zipFilename, 'r') as zFile:
        zFile.setpassword(zipPass)
        with zFile.open('known.txt') as knownFile:
            #TODO: compare contents of known.txt with actual
            validPass=True

    #Next to add file with password cannot use zipfile because password not supported
    # Note this is a linux only solution, os dependency will need to be checked
    #if compare was ok, then valid password?
    if not validPass:
        print('Invalid Password')
    else:
        #TODO: handle zipfile not-exist and existing may have to pass
        #      different flags.
        #TODO: handle filename existing in zipFilename
        #WARNING the linux manual page for 'zip' states -P is UNSECURE. 
        res = subprocess.run(['zip', '-e', '-P', zipPass, zipFilename, filename])
        #TODO: Check res for success or failure.

EDIT: I looked into fixing the whole "exposed password" issue with -P. Unfortunately, it is non trivial. You cannot simply write zipPass into the stdin of the subprocess.run with input=. I think something like pexpect might be a solution for this, but I haven't spent the time to make that work. See here for example of how to use pexpect to accomplish this: Use subprocess to send a password_

Tom Myddeltyn
  • 1,307
  • 1
  • 13
  • 27
  • 2
    "However I think since we are running process as such that the password wouldn't be somewhere easy to grab." It's trivially available in the process list (e.g. `ps aux`); that's why it's considered insecure. – AKX Dec 06 '20 at 18:19
  • I tried changing to `res = subprocess.run(['zip', '-e', zipFilename, filename], input=zipPass)` but that doesn't seem to work, unfortunately. – Tom Myddeltyn Dec 06 '20 at 18:31
  • 1
    I found an easier way around it: I asked the user for a password, got the zipfile.namelist() of the file and took the first file it finds. It will then try and extract that file with the given password, and then throws an exception if the password is obviously incorrect. Although this doesn't really work with empty archives and might be buggy for unencrypted archives, it should hold up okay. – Vasilisa Dec 07 '20 at 19:13
  • realize though that zip allows for different files to have different passwords associated with them. I'm also not sure why this doesn't support writing with passwords. While not 100% secure I don't see why it isn't implemented. – Tom Myddeltyn Dec 07 '20 at 19:26
  • Yeah I saw that as a potential issue too. This is just something that I have to write into my documentation. I will try my best to do this. – Vasilisa Dec 07 '20 at 21:16
  • So for a response as to why it isn't in the module see here: [Add encryption support to zipfile](https://bugs.python.org/issue34546) – Tom Myddeltyn Dec 07 '20 at 21:19
  • @TomMyddeltyn Thank you!! – Vasilisa Dec 11 '20 at 18:57
1

After all of the lovely replies, I did find a workaround for this just in case someone needs the answer!

I did first retry the z.testzip() and it does actually catch the bad passwords, but after seeing that it wasn't reliable (apparently hash collisions that allow for bad passwords to somehow match a small hash), I decided to use the password, extract the first file it sees in the archive, and then extract it. If it works, remove the extracted file, and if it doesn't, no harm done.

Code works as below:

try:
    z = zipfile.ZipFile(fileName, 'a') #Set zipfile object
    zipPass = bytes(zipPass, encoding='utf-8') #Str to Bytes
    z.setpassword(zipPass) #Set password
    filesInArray = z.namelist() #Get all files
    testfile = filesInArray[0] #First archive in list
    z.extract(testfile, pwd=zipPass) #Extract first file
    os.remove(testfile) #remove file if successfully extracted
except Exception as e:
    print("Exception occurred: ",repr(e))
    return None #Return to mainGUI - this exits the function without further processing

Thank you guys for the comments and answers!

Vasilisa
  • 131
  • 1
  • 10