#!/usr/local/bin/python -Ou

""" enc - encrypt/decrypt files using one of SSLeay's ciphers.

    Copyright (c) 1998 by Marc-Andre Lemburg; All Rights Reserved;
    mailto:mal@lemburg.com; See the documentation for further
    copyright information or contact the author.

    DISCLAIMER & WARNING: This tool comes with NO WARRANTY. Use at
    YOUR OWN RISK. It may destroy data ! There is NO way to recover a
    forgotten pass phrase !
"""
import exceptions,os,string,time,sys
from CryptoWorld import Ciphers,Hashes,Utils
from CommandLine import Application,SwitchOption,ArgumentOption

# Globals
verbose = 0

# Maximum block size used for en/decryption
MAX_BLOCKSIZE = 1024 * 1000

class OperationalError(exceptions.StandardError):
    pass

def filesize(file):

    oldpos = file.tell()
    file.seek(0,2)
    size = file.tell()
    file.seek(oldpos)
    return size

def invisible_input(prompt='>>> '):

    """ Adapted from the Python 1.5.1 docs example getpass()
    """
    import termios,TERMIOS
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    new = termios.tcgetattr(fd)
    new[3] = new[3] & ~TERMIOS.ECHO          # fix lflags
    try:
        termios.tcsetattr(fd, TERMIOS.TCSADRAIN, new)
        passwd = raw_input(prompt)
    finally:
        termios.tcsetattr(fd, TERMIOS.TCSADRAIN, old)
    print
    return passwd

def tempfile(filename='tmp',

             maxint=sys.maxint,time=time.time,int=int,hex=hex,
             exists=os.path.exists):

    """ Return a new filename for a temporary file (based on filename).
    """
    temp = filename + '.' + hex(maxint % int(time())) + '.tmp'
    if not exists(temp):
        return temp
    # Ok, find an alternative name
    i = 0
    while 1:
        temp = '%s.%s-%i.tmp' % (filename,hex(maxint % int(time())),i)
        if not exists(temp):
            return temp
        i = i + 1

# Global key
_key = ''

def get_cipher(name,check=0):

    global _key

    cc = getattr(Ciphers,name)
    keysize = cc.keysize
    if not _key:
        while 1:
            key1 = invisible_input('Please enter the key phrase: ')
            if check:
                key2 = invisible_input('Please reenter the phrase: ')
                if key1 != key2:
                    print "Phrases don't match. Please start again..."
                    continue
            if len(key1) == 0:
                print "Empty key phrase. Please start again..."
            else:
                break
        _key = key1
    key = _key
    # Fit key
    if keysize > 0:
        if len(key) < keysize:
            key = key + \
                  'Do not change this string, it is important !'\
                  [:keysize - len(key)]
        elif len(key) > keysize:
            key = key[:keysize]
    cipher = cc(key,Ciphers.CBC)
    return cipher

def reset_key():

    global _key

    _key = ''

###

def encrypt(filename,ciphername,overwrite=0):

    if verbose:
        print  'Encrypting:',filename
    if filename[-4:] == '.enc':
        raise OperationalError,'already encrypted'
    if not os.path.isfile(filename):
        raise OperationalError,'not a file or not found'

    # Check overwrites
    if os.path.exists(filename + '.enc'):
        if not overwrite:
            raise OperationalError,'would overwrite an existing file'
        elif os.path.samefile(filename, filename + '.enc'):
            raise OperationalError,'would overwrite the original file'

    # Open plain file
    f = open(filename,'rb')
    size = filesize(f)
    if verbose:
        print  ' total size: %i bytes' % size

    # Open work file
    workfilename = tempfile(filename)
    out = open(workfilename,'wb')

    try:
        # Init cipher and write header
        cipher = get_cipher(ciphername,check=1)
        out.write('enc %s %s %i\n' % \
                  (repr(filename),ciphername,size))

        # Init hash and blocksize
        hash = Hashes.MD5()
        blocksize = size
        if blocksize > MAX_BLOCKSIZE:
            blocksize = MAX_BLOCKSIZE
        blocksize = ((blocksize + cipher.blocksize - 1) / cipher.blocksize) \
                    * cipher.blocksize

        # Write the encrypted data in blocks
        bytesread = 0
        while bytesread < size:
            if verbose:
                print  ' reading %i bytes...' % blocksize,
            block = f.read(blocksize)
            if verbose:
                print  'read %i bytes' % len(block)
            bytesread = bytesread + len(block)
            hash.update(block)
            if bytesread == size:
                # Final block
                offset = len(block) % cipher.blocksize
                if offset:
                    padsize = cipher.blocksize - offset
                    block = block + '\0'*padsize
                    if verbose:
                        print  ' padding with %i bytes' % (padsize)
            encblock = cipher.encrypt(block)
            out.write(encblock)

        # Write hash value
        hash_value = hash.digest()
        if verbose:
            print ' hash value:',repr(hash_value)
        out.write(hash_value)

        # Copy work file to .enc file
        out.close()
        f.close()
        os.rename(workfilename,filename+'.enc')
        workfilename = None

    finally:
        if workfilename:
            if not out.closed:
                out.close()
            os.remove(workfilename)

###

def decrypt(filename,overwrite=0):

    if verbose:
        print  'Decrypting:',filename
    if filename[-4:] != '.enc':
        raise OperationalError,'decrypt a plain file'
    if not os.path.isfile(filename):
        raise OperationalError,'not a file or not found'

    # Read header from cipher file
    f = open(filename,'rb')
    header = string.split(f.readline())
    if len(header) != 4:
        raise OperationalError,'wrong header format:'+ str(header)
    origfilename = eval(header[1])
    ciphername = header[2]
    size = string.atoi(header[3])
    if verbose:
        print  ' total size: %i bytes' % size

    # Check overwrites
    if os.path.exists(origfilename):
        if not overwrite:
            raise OperationalError,'would overwrite an existing file'
        elif os.path.samefile(origfilename, filename):
            raise OperationalError,'would overwrite the encrypted file'

    # Open work file
    workfilename = tempfile(filename)
    out = open(workfilename,'wb')

    try:

        # Load cipher and init hash
        cipher = get_cipher(ciphername)
        hash = Hashes.MD5()

        # Read the encrypted data in blocks
        blocksize = size
        if blocksize > MAX_BLOCKSIZE:
            blocksize = MAX_BLOCKSIZE
        blocksize = ((blocksize + cipher.blocksize - 1) / cipher.blocksize) \
                    * cipher.blocksize
        bytesread = 0
        while bytesread < size:
            if size - bytesread < blocksize:
                # Read remaining data only
                blocksize = size - bytesread
                blocksize = ((blocksize + cipher.blocksize - 1) / \
                             cipher.blocksize) * cipher.blocksize
            if verbose:
                print  ' reading %i bytes...' % blocksize,
            encblock = f.read(blocksize)
            if verbose:
                print 'read %i bytes' % len(encblock)
            bytesread = bytesread + len(encblock)
            block = cipher.decrypt(encblock)
            if bytesread > size:
                # Depad
                padsize = bytesread - size
                block = block[:-padsize]
                if verbose:
                    print ' depadded last block by %i bytes' % (padsize)
            hash.update(block)
            out.write(block)

        # Check hash value
        hash_value = f.read(hash.digestsize)
        if verbose:
            print ' hash value:',repr(hash_value)
        if hash_value != hash.digest():
            raise OperationalError,'data corrupt'

        # Copy workfile to origfile
        out.close()
        f.close()
        os.rename(workfilename,origfilename)
        workfilename = None

    finally:
        if workfilename:
            if not out.closed:
                out.close()
            os.remove(workfilename)

###

class Encrypt(Application):

    header = "File encryption utility using the SSLeay ciphers"

    about = """\
Encrypts or decrypts the files given on the command line. If no
options are given the filenames extensions are taken as hint: '.enc'
means encrypted, everything else not encrypted. The utility then goes
and switches the state of the files. Overwriting of files only takes
place in case the '-O' switch is set.

The following ciphers are supported:
      RC2, RC4, RC5, IDEA, Blowfish, DES, DES3, CAST

This tool comes with NO WARRANTY. Use at YOUR OWN RISK. It may destroy
data ! There is NO way to recover a forgotten pass phrase !
"""

    options = [SwitchOption('-e', 'encrypt'),
               SwitchOption('-d', 'decyrpt'),
               SwitchOption('-a', 'use the same key for all files'),
               SwitchOption('-O', 'allow overwrites (use with care)'),
               ArgumentOption('-c', 'cipher to use', 'RC5'),
               ]

    def main(self):

        overwrite = self.values['-O']
        ciphername = self.values['-c']
        samekey = self.values['-a']
        for file in self.files:
            if not samekey:
                reset_key()
                print '-'*78
                print 'Working on file:',file
            try:
                if self.values['-e']:
                    encrypt(file,ciphername,overwrite)
                elif self.values['-d']:
                    decrypt(file,overwrite)
                elif file[-4:] != '.enc':
                    encrypt(file,ciphername,overwrite)
                else:
                    decrypt(file,overwrite)
            except OperationalError,why:
                print '%s skipped -- %s' % (file,why)
            except IOError,(code,why):
                print '%s skipped -- %s' % (file,why)
            except os.error,why:
                print '%s skipped -- %s' % (file,why)
            except KeyboardInterrupt:
                print '*user break*'
                break

if __name__ == '__main__':
    Encrypt()
