Encrypt files recursively with openssl

I wrote this program because I had a great idea to offload encrypted versions of my data, but conserving the full directory structure, keeping the permissions and timestamps. This way you can do incremental encrypted snapshots to an untrusted remote server. Always rsync the directory you wish to encrypt someplace else locally, and then run this script.

$ pycrypto.py -e 

This encrypts everything in the "." directory, recursively, and removes the original files. Pass the -n switch to not remove the files.

$ pyrcypto.py -d

This will decrypt everything within "." directory, with the .enc extension. Please be aware that because of the nature of the encryption used, there is no sanity check. If you enter the wrong password while decrypting, the files will be “decrypted” with the wrong password. You’ll get the files, but it won’t be the files you have encrypted in the first place. I’ve considered using a hashing method, but this comprimises security and slows down the process considerably and this was designed to be fast. You can download pycrypto here.

#!/usr/bin/env python2

"""
This script encrypts files in the current (.) directory, including 
hidden files using the AES encryption. The original timestamps are 
preserved and the original files are deleted. A .enc suffix is added 
at the end of each encrypted file. The purpose of the program is to 
encrypt the data, while preserving the original directory structure 
and timestamps so you can safely rsync it to an unsecure location. 
The passphrase needs to be either 16, 24 or 32 bytes long.

You will need to have python-crypto installed on your system, most
distributions have it in their repositories.
"""

from Crypto.Cipher import AES
from stat import *

import os, random, struct, optparse, sys, getpass

# Define options
parser = optparse.OptionParser(usage="%prog --encrypt | --decrypt")
parser.add_option('-e', '--encrypt', dest='enc', action="store_true",
       help='Encrypt the entire current directory including files in'
       ' subtrees and hidden files')
parser.add_option('-d', '--decrypt', dest='dec', action="store_true",
       help='Decrypt the entire current directory including files in'
       ' subtrees and hidden files')
parser.add_option('-v', '--verbose', dest='verb', action="store_true",
       help='Verbose mode')
parser.add_option('-n', '--no-remove', dest='delete', action="store_true",
       help='Do not remove input files once encrypted or decrypted')       
options, files = parser.parse_args()

FILES=[]

def pad_password(pwd):
    "Pad the password to lengths 16, 24, or 32, as needed for AES encryption."
    for size in 16, 24, 32:
        if len(pwd) <= size:
            return pwd + 'x' * (size - len(pwd))
    raise ValueError("password must be 32 characters, or shorter")



def encrypt_file(key, in_filename, out_filename, atime, mtime, perm, chunksize=64*1024):
    if options.verb:
        sys.stdout.write("Encrypting %s\n" % in_filename[2:])
    iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    filesize = os.path.getsize(in_filename)

    with open(in_filename, 'rb') as infile:
        with open(out_filename, 'wb') as outfile:
            outfile.write(struct.pack('<Q', filesize))
            outfile.write(iv)

            while True:
                chunk = infile.read(chunksize)
                if len(chunk) == 0:
                    break
                elif len(chunk) % 16 != 0:
                    chunk += ' ' * (16 - len(chunk) % 16)

                outfile.write(encryptor.encrypt(chunk))
    os.utime(out_filename, (atime, mtime))
    os.chmod(out_filename, perm)
    if options.delete == None:
       os.remove(in_filename)


def decrypt_file(key, in_filename, out_filename, atime, mtime, perm, chunksize=24*1024):
    if options.verb:
        sys.stdout.write("Decrypting %s\n" % in_filename[2:])

    with open(in_filename, 'rb') as infile:
        origsize = struct.unpack('<Q', infile.read(struct.calcsize('Q')))[0]
        iv = infile.read(16)
        decryptor = AES.new(key, AES.MODE_CBC, iv)

        with open(out_filename, 'wb') as outfile:
            while True:
                chunk = infile.read(chunksize)
                if len(chunk) == 0:
                    break
                outfile.write(decryptor.decrypt(chunk))

            outfile.truncate(origsize)
    os.utime(out_filename, (atime, mtime))
    os.chmod(out_filename, perm)
    if options.delete == None:
       os.remove(in_filename)

if options.enc and options.dec:
    sys.stderr.write("Please use -e or -d")
    sys.exit(1)

if options.enc == None and options.dec == None:
    sys.stderr.write("Please use -e or -d\n")
    sys.exit(1)

for dirname, dirnames, filenames in os.walk('.'):
    for filename in filenames:
        FILES.append(os.path.join(dirname, filename))

if options.enc:
    pwd = pad_password(getpass.getpass())
    for i in FILES:
       if os.path.islink(i):
           continue 
       encrypt_file(pwd, i, i+".enc",
       os.path.getatime(i), os.path.getmtime(i), os.stat(i)[ST_MODE])

if options.dec:
   pwd = pad_password(getpass.getpass())       
   for i in FILES:
       if os.path.islink(i):
           continue 
       decrypt_file(pwd, i, i[:-4], 
       os.path.getatime(i), os.path.getmtime(i), os.stat(i)[ST_MODE])

One thought on “Encrypt files recursively with openssl”

Leave a Reply