#!/usr/bin/env python
# -*- coding: ISO-8859-15 -*-
#
# Copyright (C) 2005-2007 David Guerizec <david@guerizec.net>
#
# Last modified: 2007 Dec 09, 01:33:10 by david
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA


from Crypto.Cipher import Blowfish
import base64

from config import ConfigSection, get_config

_engine_registry = {}


def register_engine(cls):
    _engine_registry[cls.cipher_id] = cls

def list_engines():
    return _engine_registry.keys()

def get_engine(cipher_type):
    engine = _engine_registry.get(cipher_type, None)
    if engine is None:
        if cipher_type != 'plain':
            raise ValueError('Unknown cipher_type %s' % cipher_type)
        engine = PlainCipher
    return engine

def cipher(text, **kw):
    if not text:
        text = ''
    engine = _default_engine[0]
    if kw.has_key('type'):
        engine = get_engine(kw['type'])
    return '%s%s' % (engine.prefix(), engine.encrypt(text, **kw))

def decipher(text, **kw):
    if not text:
        return text
    # guess the encryption method
    tokens = text.split('$', 2)

    # default decoder is Plain
    engine = PlainCipher
    if len(tokens) == 3:
        try:
            engine = get_engine(tokens[1])
            text = tokens[2]
        except ValueError:
            # maybe the text contains the magic prefix...
            # let's try to decode it as plain text
            pass

    return engine.decrypt(text, **kw)

class BaseCipher(object):
    # cipher_id = ''

    @classmethod
    def set_default(cls):
        _default_engine[0] = cls

    @classmethod
    def prefix(cls):
        return '$%s$' % cls.cipher_id

    @classmethod
    def encrypt(cls, text, **kw):
        raise NotImplementedError

    @classmethod
    def decrypt(cls, text, **kw):
        raise NotImplementedError

class PlainCipher(BaseCipher):
    cipher_id = "plain"

    @classmethod
    def prefix(cls):
        return ''

    @classmethod
    def encrypt(cls, text, **kw):
        return text

    @classmethod
    def decrypt(cls, text, **kw):
        return text

register_engine(PlainCipher)

class Base64Cipher(BaseCipher):
    cipher_id = "base64"

    @classmethod
    def encrypt(cls, text, **kw):
        return base64.b64encode(text)

    @classmethod
    def decrypt(cls, text, **kw):
        return base64.b64decode(text)

register_engine(Base64Cipher)

class BlowfishCipher(BaseCipher):
    cipher_id = "blowfish"

    @classmethod
    def set_default(cls):
        cls.engine = cls.get_engine(get_config('blowfish')['secret'])
        _default_engine[0] = cls

    @classmethod
    def get_engine(cls, secret=None):
        if secret is None:
            secret = getattr(cls, 'secret', get_config('blowfish')['secret'])
        return Blowfish.new(secret, Blowfish.MODE_ECB)

    @classmethod
    def encrypt(cls, text, **kw):
        engine = kw.get('engine', getattr(cls, 'engine', None))
        if not engine:
            engine = cls.get_engine(kw.get('secret', None))
        ftext = '%s:%s%s' % (len(text), text, '*'*8)
        ftext = ftext[:len(ftext) - (len(ftext)%8) ]
        return base64.b64encode(engine.encrypt(ftext))

    @classmethod
    def decrypt(cls, text, **kw):
        engine = kw.get('engine', getattr(cls, 'engine', None))
        if not engine:
            engine = cls.get_engine(kw.get('secret', None))
        crypted = base64.b64decode(text)
        # an exception here means we've lost the key...
        # how to handle that situation ?
        try:
            size, ftext = engine.decrypt(crypted).split(':', 1)
        except ValueError:
            import log
            log.error('Password probably encrypted with another passphrase')
            return ''
        return ftext[:int(size)]

register_engine(BlowfishCipher)

_default_engine = [ PlainCipher ]

class BlowfishConfigSection(ConfigSection):
    section_id = 'blowfish'
    section_defaults = {
        'secret': ('Enoch Root has an old cigar box on his lap.'
                   ' Golden light is shining out of the crack around its lid.'),
        }

    def __setitem__(self, option, value):
        ConfigSection.__setitem__(self, option, value)
        if option == 'secret':
            reload()

BlowfishConfigSection.register()

def _init_cipher():
    cipher_type = get_config('sshproxy')['cipher_type']
    get_engine(cipher_type).set_default()

_init_cipher()

reload = _init_cipher


#######################################################################
### Recipher database
#######################################################################


def recipher(cipher_type, password_fd, dry_run=False):
    import sys, getpass
    from sshproxy.backend import get_backend

    conf = get_config()

    newsecret = ''
    if cipher_type == 'blowfish':
        if conf['sshproxy']['cipher_type'] == 'blowfish':
            print("Recipher from blowfish to blowfish does not work "
                  "at the moment.\nPlease recipher to base64 first, "
                  "then recipher to blowfish.")
            print "Sorry for the inconvenience."
            sys.exit(0)
        try:
            if password_fd == True:
                newsecret = conf['blowfish']['secret']
            elif not password_fd:
                sec1 = 1
                sec2 = 2
                while not sec1 or sec1 != sec2:
                    sec1 = getpass.getpass("Enter secret (1/2) ")
                    sec2 = getpass.getpass("Enter secret (2/2) ")
                newsecret = sec1
            else:
                try:
                    newsecret = password_fd.readlines()[0].strip()
                except IndexError:
                    newsecret = ''
        except (KeyboardInterrupt, EOFError):
            print 'Aborted...'
            sys.exit(0)
        if len(newsecret) < 10:
            print 'Secret must be at least 10 characters long.'
            sys.exit(0)
    elif cipher_type not in ('plain', 'base64'):
        print "unknown cipher_type", cipher_type
        sys.exit(1)
    
    
    if cipher_type == 'blowfish':
        conf['blowfish']['newsecret'] = newsecret
        dry_run or conf.write() # just in case it goes wrong in the middle
    
    pwdb = get_backend()
    
    nb_passwords = 0
    nb_pkeys = 0
    total = 0
    sites = pwdb.list_site_users()
    for site in sites:
        if not site.login:
            continue
        name = site.name
        uid = site.login

        password = site.get_token('password')
        if password:
            # decipher with old secret
            oldpass = decipher(password)

            # cipher with new secret
            if oldpass is not None:
                newpass = cipher(oldpass, type=cipher_type, secret=newsecret)

            # check if we can decipher the new password
            if (oldpass is not None and
                oldpass != decipher(newpass, secret=newsecret) ):
                raise KeyError('Problem with %s cipher on %s@%s password!' %
                                                    (cipher_type, uid, name))

            # be verbose when in dry-run mode
            if dry_run and password:
                print '-- %s@%s [ %s / %s / %s ]' % (uid, name,
                                                   password, oldpass, newpass)

            # if the password changed, update it if not in dry-run mode
            if password != newpass:
                if not dry_run:
                    site.set_tokens(password=newpass)
                    site.save()
                nb_passwords += 1

        pkey = site.get_token('pkey')
        if pkey:
            # decipher with old secret
            oldpkey = decipher(pkey)

            # cipher with new secret
            if oldpkey is not None:
                newpkey = cipher(oldpkey, type=cipher_type, secret=newsecret)

            # check if we can decipher the new password
            if (oldpkey is not None and
                oldpkey != decipher(newpkey, secret=newsecret) ):
                raise KeyError('Problem with %s cipher on %s@%s pkey!' %
                                                    (cipher_type, uid, name))

            # be verbose when in dry-run mode
            if dry_run and pkey:
                print '   %s' % (pkey or '').replace('\n', '\n   ')
                print '   %s' % (oldpkey or '').replace('\n', '\n   ')
                print '   %s' % (newpkey or '').replace('\n', '\n   ')
                print 

            # if the pkey changed, update it if not in dry-run mode
            if pkey != newpkey:
                if not dry_run:
                    site.set_tokens(pkey=newpkey)
                    site.save()
                nb_pkeys += 1

        total += 1
    
    
    if cipher_type == 'blowfish':
        del conf['blowfish']['newsecret']
        # update secret
        conf['blowfish']['secret'] = newsecret
    conf['sshproxy']['cipher_type'] = cipher_type
    dry_run or conf.write()
    print 'Reciphered %d passwords and %d pkeys on %d entries' % (nb_passwords,
                                                                  nb_pkeys,
                                                                  total)