A fork of pappy proxy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
7.3 KiB

#!/usr/bin/env python
import crochet
import getpass
import glob
import os
import pappyproxy
import scrypt
import shutil
import twisted
from . import compress
from .util import PappyException
from base64 import b64encode, b64decode
from cryptography.fernet import Fernet, InvalidToken
from twisted.internet import reactor, defer
9 years ago
class Crypto(object):
def __init__(self, sessconfig):
self.config = sessconfig
9 years ago
self.archive = sessconfig.archive
self.compressor = compress.Compress(sessconfig)
self.key = None
self.password = None
self.salt = None
9 years ago
def encrypt_project(self):
"""
9 years ago
Compress and encrypt the project files,
deleting clear-text files afterwards
"""
9 years ago
# Leave the crypto working directory
if self.config.crypt_dir in os.getcwd():
os.chdir('../')
9 years ago
self.compressor.compress_project()
9 years ago
# Get the password and salt, then derive the key
self.crypto_ramp_up()
9 years ago
# Create project and crypto archive
archive_file = open(self.archive, 'rb')
archive_crypt = open(self.config.crypt_file, 'wb')
9 years ago
try:
# Encrypt the archive read as a bytestring
fern = Fernet(self.key)
crypt_token = fern.encrypt(archive_file.read())
archive_crypt.write(crypt_token)
except InvalidToken as e:
raise PappyException("Error encrypting project: ", e)
return False
9 years ago
archive_file.close()
archive_crypt.close()
9 years ago
# Store the salt for the next decryption
self.create_salt_file()
# Delete clear-text files
self.delete_clear_files()
return True
9 years ago
def decrypt_project(self):
"""
Decrypt and decompress the project files
"""
9 years ago
# 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)
project_files = self.config.get_project_files()
for pf in project_files:
shutil.copy2(pf, self.config.crypt_dir)
os.chdir(self.config.crypt_dir)
return True
9 years ago
# Otherwise, decrypt and decompress the project
else:
cf = self.config.crypt_file
sl = self.config.salt_len
crl = os.path.getsize(cf) - sl
archive_crypt = open(cf, 'rb').read(crl)
archive_file = open(self.config.archive, 'wb')
9 years ago
retries = 3
while True:
try:
9 years ago
self.crypto_ramp_up()
fern = Fernet(self.key)
archive = fern.decrypt(archive_crypt)
break
except InvalidToken as e:
print "Invalid decryption: ", e
retries -= 1
# Quit pappy if user doesn't retry
# or if all retries exhuasted
if not self.confirm_password_retry() or retries <= 0:
return False
else:
self.password = None
self.key = None
9 years ago
pass
archive_file.write(archive)
archive_file.close()
9 years ago
self.compressor.decompress_project()
9 years ago
os.chdir(self.config.crypt_dir)
return True
9 years ago
def confirm_password_retry(self):
answer = raw_input("Re-enter your password? (y/n): ").strip()
if answer[0] == "y" or answer[0] == "Y":
return True
else:
return False
9 years ago
def crypto_ramp_up(self):
if not self.password:
self.get_password()
if not self.salt:
self.set_salt()
self.derive_key()
9 years ago
def get_password(self):
"""
9 years ago
Retrieve password from the user. Raise an exception if the
password is not capable of utf-8 encoding.
"""
encoded_passwd = ""
try:
passwd = getpass.getpass("Enter a password: ").strip()
self.password = passwd.encode("utf-8")
except:
raise PappyException("Invalid password, try again")
9 years ago
def set_salt(self):
if self.config.crypt_dir in os.getcwd():
os.chdir('../')
self.set_salt_from_file()
os.chdir(self.config.crypt_dir)
elif os.path.isfile(self.config.crypt_file):
self.set_salt_from_file()
else:
9 years ago
self.salt = os.urandom(16)
def set_salt_from_file(self):
try:
# Seek to `salt_len` bytes before the EOF
# then read `salt_len` bytes to retrieve the salt
# WARNING: must open `crypt_file` in `rb` mode
# or `salt_file.seek()` will result in undefined
# behavior.
salt_file = open(self.config.crypt_file, 'rb')
sl = self.config.salt_len
# Negate the salt length to seek to the
# correct position in the buffer
salt_file.seek(-sl, 2)
self.salt = salt_file.read(sl)
salt_file.close()
except:
cf = self.config.crypt_file
raise PappyException("Unable to read %s" % cf)
9 years ago
def create_salt_file(self):
salt_file = open(self.config.crypt_file, 'a')
salt_file.write(self.salt)
salt_file.close()
9 years ago
def derive_key(self):
"""
Derive a key sufficient for use as a cryptographic key
used to encrypt the project (currently: cryptography.Fernet).
9 years ago
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/
9 years ago
Compute scrypt(password, salt, N, r, p, buflen).
9 years ago
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.
9 years ago
Notes for Python 2:
- `password` and `salt` must be str instances
- The result will be a str instance
9 years ago
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
9 years ago
Exceptions raised:
- TypeError on invalid input
- scrypt.error if scrypt failed
"""
9 years ago
try:
if not self.key:
9 years ago
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)
9 years ago
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)
9 years ago
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.crypt_file)