diff --git a/pappyproxy/compress.py b/pappyproxy/compress.py index c19761b..2204088 100644 --- a/pappyproxy/compress.py +++ b/pappyproxy/compress.py @@ -14,7 +14,8 @@ except ImportError: print "BZ2 not installed on your system" from base64 import b64encode, b64decode -from os import getcwd, sep, path, urandom +from os import getcwd, sep, path, urandom + class Compress(object): def __init__(self, sessconfig): @@ -27,17 +28,17 @@ class Compress(object): self.tar_project() else: self.zip_project() - + def decompress_project(self): if bz2: self.untar_project() else: self.unzip_project() - + def zip_project(self): """ Zip project files - + Using append mode (mode='a') will create a zip archive if none exists in the project. """ @@ -47,32 +48,32 @@ class Compress(object): zf.close() except zipfile.LargeZipFile as e: raise PappyException("Project zipfile too large. Error: ", e) - + def unzip_project(self): """ Extract project files from decrypted zip archive. Initially checks the zip archive's magic number and - attempts to extract pappy.json to validate integrity + attempts to extract pappy.json to validate integrity of the zipfile. """ if not zipfile.is_zipfile(self.zip_archive): raise PappyException("Project archive corrupted.") - + zf = zipfile.ZipFile(self.zip_archive) - + try: zf.extract("config.json") except zipfile.BadZipfile as e: - raise PappyException("Project archive contents corrupted. Error: ", e) - + raise PappyException("Zip archive corrupted. Error: ", e) + zf.extractall() - + def tar_project(self): archive = tarfile.open(self.bz2_archive, 'w:bz2') - + archive.add(self.config.crypt_dir) - archive.close() - + archive.close() + def untar_project(self): if tarfile.is_tarfile(self.bz2_archive): # Raise exception if there is a failure @@ -80,4 +81,4 @@ class Compress(object): with tarfile.open(self.bz2_archive, "r:bz2") as archive: archive.extractall() except tarfile.ExtractError as e: - raise PappyException("Project archive contents corrupted. Error: ", e) + raise PappyException("Tar archive corrupted. Error: ", e) diff --git a/pappyproxy/crypto.py b/pappyproxy/crypto.py index 56fdf7d..40359b2 100644 --- a/pappyproxy/crypto.py +++ b/pappyproxy/crypto.py @@ -14,53 +14,55 @@ from base64 import b64encode, b64decode from cryptography.fernet import Fernet, InvalidToken from twisted.internet import reactor, defer + class Crypto(object): def __init__(self, sessconfig): self.config = sessconfig - self.archive = sessconfig.archive + self.archive = sessconfig.archive self.compressor = compress.Compress(sessconfig) self.key = None self.password = None self.salt = None - + def encrypt_project(self): """ - Compress and encrypt the project files, deleting clear-text files afterwards + Compress and encrypt the project files, + deleting clear-text files afterwards """ - + # Leave the crypto working directory os.chdir('../') - + self.compressor.compress_project() - - # Create project and crypto archive - archive_file = open(self.archive, 'rb') + + # Create project and crypto archive + archive_file = open(self.archive, 'rb') archive_crypt = open(self.config.crypt_file, 'wb') - + # Get the password and salt, then derive the key self.crypto_ramp_up() - + # Encrypt the archive read as a bytestring fern = Fernet(self.key) crypt_token = fern.encrypt(archive_file.read()) archive_crypt.write(crypt_token) - + # Store the salt for the next decryption self.create_salt_file() archive_file.close() archive_crypt.close() - + # Delete clear-text files self.delete_clear_files() - - + def decrypt_project(self): """ Decrypt and decompress the project files """ - - # If project hasn't been encrypted before, setup crypt working directory + + # If project hasn't been encrypted before, + # setup crypt working directory if not os.path.isfile(self.config.crypt_file): os.mkdir(self.config.crypt_dir) @@ -69,17 +71,17 @@ class Crypto(object): shutil.copy2(pf, self.config.crypt_dir) os.chdir(self.config.crypt_dir) return True - - # Otherwise, decrypt and decompress the project - else: + + # Otherwise, decrypt and decompress the project + else: archive_crypt = open(self.config.crypt_file, 'rb').read() archive_file = open(self.config.archive, 'wb') - + retries = 3 while True: try: - self.crypto_ramp_up() - fern = Fernet(self.key) + self.crypto_ramp_up() + fern = Fernet(self.key) archive = fern.decrypt(archive_crypt) break except InvalidToken: @@ -93,35 +95,35 @@ class Crypto(object): self.password = None self.key = None self.salt = None - pass - + pass + archive_file.write(archive) archive_file.close() - + self.compressor.decompress_project() - + # Force generation of new salt and crypt archive self.delete_crypt_files() - + os.chdir(self.config.crypt_dir) return True - + def confirm_password_retry(self): - answer = raw_input("Would you like to re-enter your password? (y/n)").strip() + answer = raw_input("Re-enter your password? (y/n)").strip() if answer[0] == "y" or answer[0] == "Y": return True else: return False - + def crypto_ramp_up(self): if not self.password: self.get_password() self.set_salt() self.derive_key() - + def get_password(self): """ - Retrieve password from the user. Raise an exception if the + Retrieve password from the user. Raise an exception if the password is not capable of utf-8 encoding. """ encoded_passwd = "" @@ -130,75 +132,75 @@ class Crypto(object): self.password = passwd.encode("utf-8") except: raise PappyException("Invalid password, try again") - + def set_salt(self): if os.path.isfile(self.config.salt_file): self.set_salt_from_file() else: - self.salt = os.urandom(16) - + self.salt = os.urandom(16) + def set_salt_from_file(self): try: salt_file = open(self.config.salt_file, 'rb') self.salt = salt_file.readline().strip() except: raise PappyException("Unable to read project.salt") - + def create_salt_file(self): salt_file = open(self.config.salt_file, 'wb') - + salt_file.write(self.salt) salt_file.close() - + def derive_key(self): """ Derive a key sufficient for use as a cryptographic key used to encrypt the project (currently: cryptography.Fernet). - + cryptography.Fernet utilizes AES-CBC-128, requiring a 32-byte key. Parameter notes from the py-scrypt source-code: https://bitbucket.org/mhallin/py-scrypt/ - + Compute scrypt(password, salt, N, r, p, buflen). - + The parameters r, p, and buflen must satisfy r * p < 2^30 and buflen <= (2^32 - 1) * 32. The parameter N must be a power of 2 greater than 1. N, r and p must all be positive. - + Notes for Python 2: - `password` and `salt` must be str instances - The result will be a str instance - + Notes for Python 3: - `password` and `salt` can be both str and bytes. If they are str instances, they wil be encoded with utf-8. - The result will be a bytes instance - + Exceptions raised: - TypeError on invalid input - scrypt.error if scrypt failed """ - + try: if not self.key: - self.key = b64encode(scrypt.hash(self.password, self.salt, buflen=32)) + shash = scrypt.hash(self.password, self.salt, buflen=32) + self.key = b64encode(shash) except TypeError as e: raise PappyException("Scrypt failed with type error: ", e) except scrypt.error, e: raise PappyException("Scrypt failed with internal error: ", e) - + def delete_clear_files(self): """ Deletes all clear-text files left in the project directory. """ shutil.rmtree(self.config.crypt_dir) os.remove(self.config.archive) - + def delete_crypt_files(self): """ Deletes all encrypted-text files in the project directory. Forces generation of new salt after opening and closing the project. Adds security in the case of a one-time compromise of the system. """ - #os.remove(self.config.salt_file) os.remove(self.config.crypt_file) diff --git a/pappyproxy/pappy.py b/pappyproxy/pappy.py index 024709f..364fa07 100755 --- a/pappyproxy/pappy.py +++ b/pappyproxy/pappy.py @@ -159,9 +159,9 @@ class PappySession(object): yield True # Quit pappy on failure else: - reactor.stop() + reactor.stop() defer.returnValue(None) - + @defer.inlineCallbacks def cleanup(self, ignored=None): for port in self.ports: @@ -181,10 +181,13 @@ def parse_args(): parser = argparse.ArgumentParser(description='An intercepting proxy for testing web applications.') parser.add_argument('-l', '--lite', help='Run the proxy in "lite" mode', action='store_true') try: - parser.add_argument('-c', '--crypt', type=str, nargs=1, help='Start pappy in "crypto" mode, must supply a name for the encrypted project archive [CRYPT]') + hlpmsg = 'Start pappy in "crypto" mode,'+ + 'must supply a name for the encrypted'+ + 'project archive [CRYPT]' + parser.add_argument('-c', '--crypt', type=str, nargs=1, help=hlpmsg) except: print 'Must supply a project name: pappy -c ' - reactor.stop() + reactor.stop() defer.returnValue(None) args = parser.parse_args(sys.argv[1:]) @@ -197,7 +200,7 @@ def parse_args(): if args.crypt: # Convert from single-item list produced by argparse `nargs=1` - settings['crypt'] = args.crypt[0].encode('utf-8') + settings['crypt'] = args.crypt[0].encode('utf-8') else: settings['crypt'] = None