diff --git a/HISTORY.rst b/HISTORY.rst index efa3ad2..a058c0c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ History =============== +0.2.1 (Feb 2017) +~~~~~~~~~~~~~~~~~~ +* Better exceptions handling +* Code clean-up +* More unittests + 0.2 (Jan 2017) ~~~~~~~~~~~~~~~~~~ * Modularized pyAesCrypt (and now the script calls the module for operations) diff --git a/README.rst b/README.rst index 33cb175..4c5774c 100644 --- a/README.rst +++ b/README.rst @@ -53,4 +53,4 @@ Decrypt file test.txt.aes in test2.txt: .. _AES Crypt: https://www.aescrypt.com .. _file format: https://www.aescrypt.com/aes_file_format.html -.. _Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 \ No newline at end of file +.. _Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/THANKS.rst b/THANKS.rst index a16e042..68d01e7 100644 --- a/THANKS.rst +++ b/THANKS.rst @@ -2,4 +2,4 @@ Thanks =============== Thanks to `Ben Fisher`_ for providing a patch to improve decryption speed. -.. _Ben Fisher: https://downpoured.github.io/ \ No newline at end of file +.. _Ben Fisher: https://downpoured.github.io/ diff --git a/bin/pyAesCrypt b/bin/pyAesCrypt index d5f005f..e072fde 100755 --- a/bin/pyAesCrypt +++ b/bin/pyAesCrypt @@ -2,13 +2,13 @@ # #============================================================================== # Copyright 2016 Marco Bellaccini - marco.bellaccini[at!]gmail.com -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,16 +17,16 @@ #============================================================================== #============================================================================== -# pyAesCrypt 0.2 -# -# pyAesCrypt is a Python file-encryption utility that uses AES256-CBC to +# pyAesCrypt +# +# pyAesCrypt is a Python file-encryption utility that uses AES256-CBC to # encrypt/decrypt files. # pyAesCrypt is compatible with the AES Crypt (https://www.aescrypt.com/) # file format (version 2). -# -# IMPORTANT SECURITY NOTE: version 2 of the AES Crypt file format does not -# authenticate the "file size modulo 16" byte. This implies that an attacker -# with write access to the encrypted file may alter the corresponding plaintext +# +# IMPORTANT SECURITY NOTE: version 2 of the AES Crypt file format does not +# authenticate the "file size modulo 16" byte. This implies that an attacker +# with write access to the encrypted file may alter the corresponding plaintext # file size by up to 15 bytes. # # NOTE: there is no low-level memory management in Python, hence it is @@ -38,30 +38,37 @@ import argparse import getpass from sys import exit +from os.path import isfile import pyAesCrypt -maxPassLen = 1024 # maximum password length (number of chars) +maxPassLen = 1024 # maximum password length (number of chars) # encryption/decryption buffer size - 64K bufferSize = 64 * 1024 # parse command line arguments -parser = argparse.ArgumentParser(description="Encrypt/decrypt a file using AES256-CBC.") +parser = argparse.ArgumentParser(description=("Encrypt/decrypt a file " + "using AES256-CBC.")) parser.add_argument("filename", type=str, help="file to encrypt/decrypt") -parser.add_argument("-o","--out", type=str, +parser.add_argument("-o", "--out", type=str, default=None, help="specify output file") # encrypt OR decrypt.... -groupED = parser.add_mutually_exclusive_group(required=True) +groupED = parser.add_mutually_exclusive_group(required=True) groupED.add_argument("-e", "--encrypt", - help="encrypt file", action="store_true") + help="encrypt file", action="store_true") groupED.add_argument("-d", "--decrypt", - help="decrypt file", action="store_true") + help="decrypt file", action="store_true") args = parser.parse_args() + +# check for input file existence +if not isfile(args.filename): + exit("Error: file \"" + args.filename + "\" was not found.") + # prompt the user for password -passw=str(getpass.getpass("Password:")) +passw = str(getpass.getpass("Password:")) if args.encrypt: # check against max password length @@ -76,15 +83,16 @@ if args.encrypt: # 1 digit # 1 symbol if not((len(passw) > 7) and any(c.islower() for c in passw) - and any(c.isupper() for c in passw) and any(c.isdigit() for c in passw) - and any(not(c.isalnum()) for c in passw)): + and any(c.isupper() for c in passw) + and any(c.isdigit() for c in passw) + and any(not(c.isalnum()) for c in passw)): print("Warning: your password seems weak.") print("A password should be at least 8 chars long and should " - "contain at least one lowercase char, one uppercase char, one " - "digit and one symbol.") + "contain at least one lowercase char, one uppercase char, " + "one digit and one symbol.") # re-prompt the user for password - passwConf=str(getpass.getpass("Confirm password:")) + passwConf = str(getpass.getpass("Confirm password:")) # check the second pass against the first if passw != passwConf: @@ -97,7 +105,14 @@ if args.encrypt: ofname = args.filename+".aes" # call encryption function - pyAesCrypt.encryptFile(args.filename, ofname, passw, bufferSize) + try: + pyAesCrypt.encryptFile(args.filename, ofname, passw, bufferSize) + # handle IO errors + except IOError as ex: + exit(ex) + # handle value errors + except ValueError as ex: + exit(ex) elif args.decrypt: # open output file @@ -107,7 +122,14 @@ elif args.decrypt: ofname = args.filename[:-4] else: exit("Error: if input file extension is not \".aes\", you should " - "provide the output file name through \"-o\" option.") + "provide the output file name through \"-o\" option.") # call decryption function - pyAesCrypt.decryptFile(args.filename, ofname, passw, bufferSize) \ No newline at end of file + try: + pyAesCrypt.decryptFile(args.filename, ofname, passw, bufferSize) + # handle IO errors + except IOError as ex: + exit(ex) + # handle value errors + except ValueError as ex: + exit(ex) diff --git a/pyAesCrypt/__init__.py b/pyAesCrypt/__init__.py index 89a864d..ad3217a 100644 --- a/pyAesCrypt/__init__.py +++ b/pyAesCrypt/__init__.py @@ -1 +1 @@ -from .crypto import encryptFile, decryptFile \ No newline at end of file +from .crypto import encryptFile, decryptFile diff --git a/pyAesCrypt/crypto.py b/pyAesCrypt/crypto.py index 8a94d7f..fda7837 100644 --- a/pyAesCrypt/crypto.py +++ b/pyAesCrypt/crypto.py @@ -1,12 +1,12 @@ #============================================================================== # Copyright 2016 Marco Bellaccini - marco.bellaccini[at!]gmail.com -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,16 +15,16 @@ #============================================================================== #============================================================================== -# pyAesCrypt 0.2 -# -# pyAesCrypt is a Python file-encryption utility that uses AES256-CBC to +# pyAesCrypt +# +# pyAesCrypt is a Python file-encryption utility that uses AES256-CBC to # encrypt/decrypt files. # pyAesCrypt is compatible with the AES Crypt (https://www.aescrypt.com/) # file format (version 2). -# -# IMPORTANT SECURITY NOTE: version 2 of the AES Crypt file format does not -# authenticate the "file size modulo 16" byte. This implies that an attacker -# with write access to the encrypted file may alter the corresponding plaintext +# +# IMPORTANT SECURITY NOTE: version 2 of the AES Crypt file format does not +# authenticate the "file size modulo 16" byte. This implies that an attacker +# with write access to the encrypted file may alter the corresponding plaintext # file size by up to 15 bytes. # # NOTE: there is no low-level memory management in Python, hence it is @@ -37,10 +37,11 @@ from Crypto.Hash import HMAC from Crypto.Cipher import AES from Crypto import Random -from sys import exit from os import stat from os import remove -import atexit + +# pyAesCrypt version +version = "0.2.1" # encryption/decryption buffer size - 64K bufferSize = 64 * 1024 @@ -48,16 +49,18 @@ # maximum password length (number of chars) maxPassLen = 1024 + # password stretching function def stretch(passw, iv1): + # hash the external iv and the password 8192 times - digest=iv1+(16*b"\x00") + digest = iv1 + (16 * b"\x00") for i in range(8192): - passHash=SHA256.new() + passHash = SHA256.new() passHash.update(digest) - passHash.update(bytes(passw,"utf_16_le")) - digest=passHash.digest() + passHash.update(bytes(passw, "utf_16_le")) + digest = passHash.digest() return digest @@ -67,24 +70,32 @@ def stretch(passw, iv1): # infile: plaintext file path # outfile: ciphertext file path # passw: encryption password -# bufferSize: encryption buffer size, must be a multiple of AES block size (16) -# using a larger buffer speeds up things when dealing with big files +# bufferSize: encryption buffer size, must be a multiple of +# AES block size (16) +# using a larger buffer speeds up things when dealing +# with big files def encryptFile(infile, outfile, passw, bufferSize): - assert bufferSize % AES.block_size == 0, "Buffer size must be a multiple of AES block size." - assert len(passw) <= maxPassLen, "Password is too long." + # validate bufferSize + if bufferSize % AES.block_size != 0: + raise ValueError("Buffer size must be a multiple of AES block size.") + + if len(passw) > maxPassLen: + raise ValueError("Password is too long.") + try: - with open(infile,"rb") as fIn: + with open(infile, "rb") as fIn: # initialize random number generator - # using pycrypto cryptographic PRNG (based on "Fortuna" by N. Ferguson - # and B. Schneier, with the OS RNG, time.clock() and time.time() as - # entropy sources) + # using pycrypto cryptographic PRNG (based on "Fortuna" by + # N. Ferguson and B. Schneier, with the OS RNG, time.clock() + # and time.time() as entropy sources) rng = Random.new() - # generate external iv (used to encrypt the main iv and the encryption key) + # generate external iv (used to encrypt the main iv and the + # encryption key) iv1 = rng.read(AES.block_size) # stretch password and iv - key=stretch(passw, iv1) + key = stretch(passw, iv1) # generate random main iv iv0 = rng.read(AES.block_size) @@ -109,24 +120,26 @@ def encryptFile(infile, outfile, passw, bufferSize): hmac1.update(c_iv_key) try: - with open(outfile,"wb") as fOut: + with open(outfile, "wb") as fOut: # write header fOut.write(bytes("AES", "utf8")) - # write version (AES Crypt version 2 file format: https://www.aescrypt.com/aes_file_format.html) + # write version (AES Crypt version 2 file format - + # see https://www.aescrypt.com/aes_file_format.html) fOut.write(b"\x02") # reserved byte (set to zero) fOut.write(b"\x00") # setup "CREATED-BY" extension - cby="pyAesCrypt 0.2" + cby = "pyAesCrypt " + version # write "CREATED-BY" extension length fOut.write(b"\x00" + bytes([1+len("CREATED_BY"+cby)])) # write "CREATED-BY" extension - fOut.write(bytes("CREATED_BY", "utf8") + b"\x00" + bytes(cby, "utf8")) + fOut.write(bytes("CREATED_BY", "utf8") + b"\x00" + + bytes(cby, "utf8")) # write "container" extension length fOut.write(b"\x00\x80") @@ -138,7 +151,8 @@ def encryptFile(infile, outfile, passw, bufferSize): # write end-of-extensions tag fOut.write(b"\x00\x00") - # write the iv used to encrypt the main iv and the encryption key + # write the iv used to encrypt the main iv and the + # encryption key fOut.write(iv1) # write encrypted main iv and key @@ -160,7 +174,8 @@ def encryptFile(infile, outfile, passw, bufferSize): # file size mod 16, lsb positions fs16 = bytes([bytesRead % AES.block_size]) # pad data (this is NOT PKCS#7!) - # ...unless no bytes or a multiple of a block size of bytes was read + # ...unless no bytes or a multiple of a block size + # of bytes was read if bytesRead % AES.block_size == 0: padLen = 0 else: @@ -183,7 +198,6 @@ def encryptFile(infile, outfile, passw, bufferSize): # write encrypted file content fOut.write(cText) - # write plaintext file size mod 16 lsb positions fOut.write(fs16) @@ -191,37 +205,48 @@ def encryptFile(infile, outfile, passw, bufferSize): fOut.write(hmac0.digest()) except IOError: - exit("Error: unable to write output file.") + raise IOError("Unable to write output file.") except IOError: - exit("Error: file \"" + infile + "\" was not found.") + raise IOError("File \"" + infile + "\" was not found.") + # decrypting function # arguments: # infile: ciphertext file path # outfile: plaintext file path # passw: encryption password # bufferSize: decryption buffer size, must be a multiple of AES block size (16) -# using a larger buffer speeds up things when dealing with big files +# using a larger buffer speeds up things when dealing with +# big files def decryptFile(infile, outfile, passw, bufferSize): - assert bufferSize % AES.block_size == 0, "Buffer size must be a multiple of AES block size" - assert len(passw) <= maxPassLen, "Password is too long." + # validate bufferSize + if bufferSize % AES.block_size != 0: + raise ValueError("Buffer size must be a multiple of AES block size") + + if len(passw) > maxPassLen: + raise ValueError("Password is too long.") + + # get input file size + inputFileSize = stat(infile).st_size + try: - with open(infile,"rb") as fIn: + with open(infile, "rb") as fIn: fdata = fIn.read(3) # check if file is in AES Crypt format (also min length check) - if fdata != bytes("AES","utf8") or stat(infile).st_size < 136: - exit("Error: file is corrupted or " - "not an AES Crypt (or pyAesCrypt) file.") + if (fdata != bytes("AES", "utf8") or inputFileSize < 136): + raise ValueError("File is corrupted or not an AES Crypt " + "(or pyAesCrypt) file.") # check if file is in AES Crypt format, version 2 # (the only one compatible with pyAesCrypt) fdata = fIn.read(1) - if len(fdata) < 1: - exit("Error: file is corrupted.") + if len(fdata) != 1: + raise ValueError("File is corrupted.") + if fdata != b"\x02": - exit("Error: pyAesCrypt is only compatible with version 2 of the " - "AES Crypt file format.") + raise ValueError("pyAesCrypt is only compatible with version " + "2 of the AES Crypt file format.") # skip reserved byte fIn.read(1) @@ -229,29 +254,29 @@ def decryptFile(infile, outfile, passw, bufferSize): # skip all the extensions while True: fdata = fIn.read(2) - if len(fdata) < 2: - exit("Error: file is corrupted.") + if len(fdata) != 2: + raise ValueError("File is corrupted.") if fdata == b"\x00\x00": break fIn.read(int.from_bytes(fdata, byteorder="big")) # read external iv iv1 = fIn.read(16) - if len(iv1) < 16: - exit("Error: file is corrupted.") + if len(iv1) != 16: + raise ValueError("File is corrupted.") # stretch password and iv - key=stretch(passw, iv1) + key = stretch(passw, iv1) # read encrypted main iv and key c_iv_key = fIn.read(48) - if len(c_iv_key) < 48: - exit("Error: file is corrupted.") + if len(c_iv_key) != 48: + raise ValueError("File is corrupted.") # read HMAC-SHA256 of the encrypted iv and key hmac1 = fIn.read(32) - if len(hmac1) < 32: - exit("Error: file is corrupted.") + if len(hmac1) != 32: + raise ValueError("File is corrupted.") # compute actual HMAC-SHA256 of the encrypted iv and key hmac1Act = HMAC.new(key, digestmod=SHA256) @@ -259,7 +284,7 @@ def decryptFile(infile, outfile, passw, bufferSize): # HMAC check if hmac1 != hmac1Act.digest(): - exit("Error: wrong password (or file is corrupted).") + raise ValueError("Wrong password (or file is corrupted).") # instantiate AES cipher cipher1 = AES.new(key, AES.MODE_CBC, iv1) @@ -272,20 +297,13 @@ def decryptFile(infile, outfile, passw, bufferSize): intKey = iv_key[16:] # instantiate another AES cipher - cipher0 = AES.new(intKey, AES.MODE_CBC, iv0) + cipher0 = AES.new(intKey, AES.MODE_CBC, iv0) # instantiate actual HMAC-SHA256 of the ciphertext hmac0Act = HMAC.new(intKey, digestmod=SHA256) try: - with open(outfile,"wb") as fOut: - # register delete output file as cleanup function - # (so that, if fails, deletes output file) - atexit.register(remove, outfile) - - # get input file size - inputFileSize = stat(infile).st_size - + with open(outfile, "wb") as fOut: while fIn.tell() < inputFileSize - 32 - 1 - bufferSize: # read data cText = fIn.read(bufferSize) @@ -305,10 +323,14 @@ def decryptFile(infile, outfile, passw, bufferSize): # last block reached, remove padding if needed # read last block - if fIn.tell() != inputFileSize - 32 - 1: # this is for empty files + + # this is for empty files + if fIn.tell() != inputFileSize - 32 - 1: cText = fIn.read(AES.block_size) if len(cText) < AES.block_size: - exit("Error: file is corrupted.") + # remove outfile and raise exception + remove(outfile) + raise ValueError("File is corrupted.") else: cText = bytes() @@ -317,34 +339,37 @@ def decryptFile(infile, outfile, passw, bufferSize): # read plaintext file size mod 16 lsb positions fs16 = fIn.read(1) - if len(fs16) < 1: - exit("Error: file is corrupted.") + if len(fs16) != 1: + # remove outfile and raise exception + remove(outfile) + raise ValueError("File is corrupted.") # decrypt last block pText = cipher0.decrypt(cText) # remove padding - toremove=((16-fs16[0])%16) + toremove = ((16 - fs16[0]) % 16) if toremove != 0: - pText=pText[:-toremove] + pText = pText[:-toremove] # write decrypted data to output file fOut.write(pText) # read HMAC-SHA256 of the encrypted file hmac0 = fIn.read(32) - if len(hmac0) < 32: - exit("Error: file is corrupted.") - + if len(hmac0) != 32: + # remove outfile and raise exception + remove(outfile) + raise ValueError("File is corrupted.") + # HMAC check if hmac0 != hmac0Act.digest(): - exit("Error: bad HMAC (file is corrupted).") - - # unregister output file removal - # i.e.: success, output file is valid - atexit.unregister(remove) + # remove outfile and raise exception + remove(outfile) + raise ValueError("Bad HMAC (file is corrupted).") except IOError: - exit("Error: unable to write output file.") + raise IOError("Unable to write output file.") + except IOError: - exit("Error: file \"" + infile + "\" was not found.") \ No newline at end of file + raise IOError("File \"" + infile + "\" was not found.") diff --git a/pyAesCrypt/test_crypto.py b/pyAesCrypt/test_crypto.py index 0b94dda..36e2aab 100644 --- a/pyAesCrypt/test_crypto.py +++ b/pyAesCrypt/test_crypto.py @@ -1,12 +1,12 @@ #============================================================================== # Copyright 2016 Marco Bellaccini - marco.bellaccini[at!]gmail.com -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,6 +21,8 @@ import shutil import filecmp import subprocess +from os import stat +from os.path import isfile import pyAesCrypt # test file directory name @@ -36,13 +38,14 @@ decsuffix = '.decr' # file names -filenames = [prefix+'empty', prefix+'block', prefix+'defbuffer', prefix+'small', prefix+'med', prefix+'tbuf'] +filenames = [prefix+'empty', prefix+'block', prefix+'defbuffer', + prefix+'small', prefix+'med', prefix+'tbuf'] # generate encrypted file names encfilenames = list() for f in filenames: encfilenames.append(f+encsuffix) - + # generate decrypted file names decfilenames = list() for f in filenames: @@ -54,7 +57,8 @@ # test password password = "foopassword!1$A" -# generate test files + +# function for generating test files def genTestFiles(): # empty file with open(filenames[0], 'wb') as fout: @@ -74,8 +78,27 @@ def genTestFiles(): # 3-buffer sized file with open(filenames[5], 'wb') as fout: fout.write(os.urandom(3*bufferSize)) - -# tests encryption and decryption + + +# function for corrupting files +def corruptFile(fp, offset): + # open file + with open(fp, 'r+b') as ftc: + # get to the byte to corrupt + ftc.seek(offset) + # read the byte + rb = ftc.read(1) + # back to the byte to corrupt + ftc.seek(offset) + # if byte is 'X', overwrite with 'Y' + if rb == b'\x58': + ftc.write(b'\x59') + # else overwrite with 'X' + else: + ftc.write(b'\x58') + + +# test encryption and decryption # NOTE: SOME TESTS REQUIRE AES CRYPT INSTALLED, WITH ITS BINARY IN $PATH class TestEncDec(unittest.TestCase): # fixture for preparing the environment @@ -108,7 +131,7 @@ def test_enc_pyAesCrypt_dec_pyAesCrypt(self): # decrypt file pyAesCrypt.decryptFile(ct, ou, password, bufferSize) # check that the original file and the output file are equal - self.assertTrue(filecmp.cmp(pt,ou)) + self.assertTrue(filecmp.cmp(pt, ou)) # test encryption with pyAesCrypt and decryption with AES Crypt def test_enc_pyAesCrypt_dec_AesCrypt(self): @@ -118,7 +141,7 @@ def test_enc_pyAesCrypt_dec_AesCrypt(self): # decrypt file subprocess.call(["aescrypt", "-d", "-p", password, "-o", ou, ct]) # check that the original file and the output file are equal - self.assertTrue(filecmp.cmp(pt,ou)) + self.assertTrue(filecmp.cmp(pt, ou)) # test encryption with AES Crypt and decryption with pyAesCrypt def test_enc_AesCrypt_dec_pyAesCrypt(self): @@ -128,8 +151,131 @@ def test_enc_AesCrypt_dec_pyAesCrypt(self): # decrypt file pyAesCrypt.decryptFile(ct, ou, password, bufferSize) # check that the original file and the output file are equal - self.assertTrue(filecmp.cmp(pt,ou)) + self.assertTrue(filecmp.cmp(pt, ou)) + +# test exceptions +class TestExceptions(unittest.TestCase): + + # test file path + tfile = prefix+'test.txt' + + # fixture for preparing the environment + def setUp(self): + # make directory for test files + try: + os.mkdir(tfdirname) + # if directory exists, delete and re-create it + except FileExistsError: + # remove whole tree + shutil.rmtree(tfdirname) + os.mkdir(tfdirname) + # generate a test file + with open(self.tfile, 'wb') as fout: + fout.write(os.urandom(4)) + + def tearDown(self): + # remove whole directory tree + shutil.rmtree(tfdirname) + + # test decryption with wrong password + def test_dec_wrongpass(self): + # encrypt file + pyAesCrypt.encryptFile(self.tfile, self.tfile+'.aes', password, + bufferSize) + # try to decrypt file using a wrong password + # and check that ValueError is raised + self.assertRaisesRegex(ValueError, ("Wrong password " + "\(or file is corrupted\)."), + pyAesCrypt.decryptFile, + self.tfile + '.aes', self.tfile + '.decr', + 'wrongpass', bufferSize) + + # check that decrypted file was not created + self.assertFalse(isfile(self.tfile + '.decr')) + + # test decryption of a non-AES-Crypt-format file + def test_dec_not_AesCrypt_format(self): + # encrypt file + pyAesCrypt.encryptFile(self.tfile, self.tfile+'.aes', password, + bufferSize) + # corrupt the 2nd byte (the 'E' of 'AES') - offset is 2-1=1 + corruptFile(self.tfile+'.aes', 1) + + # try to decrypt file + # ...and check that ValueError is raised + self.assertRaisesRegex(ValueError, ("File is corrupted or " + "not an AES Crypt " + "\(or pyAesCrypt\) file."), + pyAesCrypt.decryptFile, + self.tfile + '.aes', self.tfile + '.decr', + password, bufferSize) + + # check that decrypted file was not created + self.assertFalse(isfile(self.tfile + '.decr')) + + # test decryption of an unsupported version of AES Crypt format + def test_dec_unsupported_AesCrypt_format(self): + # encrypt file + pyAesCrypt.encryptFile(self.tfile, self.tfile+'.aes', password, + bufferSize) + # corrupt the 4th byte + corruptFile(self.tfile+'.aes', 3) + + # try to decrypt file + # ...and check that ValueError is raised + self.assertRaisesRegex(ValueError, ("pyAesCrypt is only " + "compatible with version 2 of " + "the AES Crypt file format."), + pyAesCrypt.decryptFile, self.tfile + '.aes', + self.tfile + '.decr', password, bufferSize) + + # check that decrypted file was not created + self.assertFalse(isfile(self.tfile + '.decr')) + + # test decryption of a file with bad hmac + def test_dec_bad_hmac(self): + # encrypt file + pyAesCrypt.encryptFile(self.tfile, self.tfile+'.aes', password, + bufferSize) + + # get file size + fsize = stat(self.tfile+'.aes').st_size + + # corrupt hmac + corruptFile(self.tfile+'.aes', fsize-1) + + # try to decrypt file + # ...and check that ValueError is raised + self.assertRaisesRegex(ValueError, ("Bad HMAC " + "\(file is corrupted\)."), + pyAesCrypt.decryptFile, self.tfile + '.aes', + self.tfile + '.decr', password, bufferSize) + + # check that decrypted file was deleted + self.assertFalse(isfile(self.tfile + '.decr')) + + # test decryption of a truncated file (no complete hmac) + def test_dec_trunc_file(self): + # encrypt file + pyAesCrypt.encryptFile(self.tfile, self.tfile+'.aes', password, + bufferSize) + + # get file size + fsize = stat(self.tfile+'.aes').st_size + + # truncate hmac (i.e.: truncate end of the file) + with open(self.tfile+'.aes', 'r+b') as ftc: + ftc.truncate(fsize-1) + + # try to decrypt file + # ...and check that ValueError is raised + self.assertRaisesRegex(ValueError, "File is corrupted.", + pyAesCrypt.decryptFile, self.tfile + '.aes', + self.tfile + '.decr', password, bufferSize) + + # check that decrypted file was deleted + self.assertFalse(isfile(self.tfile + '.decr')) + if __name__ == '__main__': unittest.main() - \ No newline at end of file diff --git a/setup.py b/setup.py index 2be71f4..29521fd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ README = readme.read() setup(name='pyAesCrypt', - version='0.2', + version='0.2.1', packages = find_packages(), include_package_data=True, description='Encrypt and decrypt files in AES Crypt format (version 2)',