# -*- coding: ascii -*-

###########################################################################
# clive, video extraction utility
# Copyright (C) 2007-2008 Toni Gundogdu
#
# This file is part of clive.
#
# clive 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 3 of the License, or
# (at your option) any later version.
#
# clive 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 clive.  If not, see <http://www.gnu.org/licenses/>.
###########################################################################

## The classes for handling ~/.clive/passwd file

# Do not cache these imports: they are not used in any other files
import getpass
import pwd
import base64
import stat

__all__ = ['Passwd']

from clive.path import ConfigDir
from clive.modules import Modules

## The passwd file class
class Passwd:

    ## Constructor
    def __init__(self):
        self._os = Modules().getinst('os')
        self._pycrypto = Modules().getinst('pycrypto')
        self._version = '1.0.0'
        self._salt = getpass.getuser() # use username + uid
        self._salt += '%x' % pwd.getpwnam(self._salt)[2]
        self._fn = ConfigDir().passwdfile()
        self._file_version = None
        self._file_hash = None
        self._entries = []
        self._load_file()

    def __del__(self):
        if self._os.path.exists(self._fn):
            self._os.chmod(self._fn, stat.S_IRUSR|stat.S_IWUSR) # 0600

    ## Add new password entry
    def add_entry(self, entry):
        if self.check_entry(entry):
            raise SystemExit('error: found duplicate entry with same name')
        (phrase_hash, cipher_key) = self._getpassphrase()
        if self._file_hash != None:
            if not phrase_hash == self._file_hash:
                raise SystemExit('error: incorrect passphrase')
        while 1:
            usern = raw_input('Enter login username for %s:' % entry)
            if len(usern) > 4: break
        while 1:
            # NOTE: Passwords are padded with whitespace to 32 char length
            passw = self._getpass('Enter password for %s:' % usern)
            if len(passw) < 4: continue
            passw = passw.ljust(32)
            again = self._getpass('Re-enter the password:').ljust(32)
            if passw == again: break
            else: print 'error: passwords do not match'
        o = self._pycrypto.new(cipher_key, self._pycrypto.MODE_ECB)
        epassw = base64.b64encode(o.encrypt(passw))
        mode = ['w','a'][len(self._entries) > 0]
        f = open(self._fn, mode) # Append the password to file
        if mode == 'w':
            f.write('passwd_version="%s"\n' % self._version)
            f.write('passwd_hash="%s"\n' % phrase_hash)
        f.write('%s="%s:%s"\n' % (entry, usern, epassw))
        f.flush()
        f.close()

    ## Delete an existing entry
    def delete_entry(self, entry):
        if not self.check_entry(entry):
            raise SystemExit('error: entry "%s" does not exist' % entry)
        a = raw_input('> Confirm delete? (y/N):')
        if a.lower() != 'y': raise SystemExit
        f = open(self._fn, 'w') # Rewrite file without the entry
        f.write('passwd_version="%s"\n' % self._version)
        f.write('passwd_hash="%s"\n' % self._file_hash)
        for (e,v) in self._entries:
            if e != entry:
                (usern,passw) = v.split(':',1)
                f.write('%s="%s:%s"\n' % (e,usern,passw))
        f.flush()
        f.close()

    ## Check if the specified password entry exists
    def check_entry(self, entry):
        for (name,value) in self._entries:
            if name == entry: return True
        return False            

    ## Get the specified password entry
    # \param entry Entry name
    # \param use_phrase A passphrase, if used, skips prompting for it
    def get_entry(self, entry, use_phrase=None):
        if not self.check_entry(entry):
            raise SystemExit('error: entry "%s" not found' % entry)
        (phrase_hash, cipher_key) = self._getpassphrase(use_phrase)
        if not phrase_hash == self._file_hash:
            raise SystemExit('error: incorrect passphrase')
        for (e,v) in self._entries:
            if e == entry:
                (usern,passw) = v.split(':',1)
                o = self._pycrypto.new(cipher_key, self._pycrypto.MODE_ECB)
                passw = o.decrypt(base64.b64decode(passw))
                return (usern,passw.rstrip())
        return None                

    ## Returns loaded entries
    def entries(self):
        return self._entries

    def _getpass(self, prompt):
        sys = Modules().getinst('sys')
        if sys.stdin.isatty():
            r = getpass.getpass(prompt)
        else: # No tty: use newt since we have it available
            newt = Modules().getinst('newt')
            try:
                scr = newt.SnackScreen()
                self._e = newt.Entry(32, password=1)
                g = newt.GridForm(scr, prompt, 1,2)
                g.add(self._e, col=0, row=0, padding=(0,0,0,1))
                b = newt.ButtonBar(scr, [('Cancel',0), ('OK',1)], compact=0)
                g.add(b, col=0, row=1, padding=(0,0,0,0))
                d = {0:self._cancel, 1:self._ok, None:self._ok}
                r = d.get(b.buttonPressed(g.runOnce()))()
            finally:
                scr.finish()
        return r

    def _cancel(self):
        raise SystemExit('Cancelled.')

    def _ok(self):
        return self._e.value()

    def _getpassphrase(self, use_phrase=None):
        if not use_phrase:
            create = self._file_hash == None
            if create: print 'creating', self._fn
            verify = 0
            while 1:
                if not verify:
                    phrase = \
                        self._getpass('Enter passphrase for %s:' % self._fn)
                    if len(phrase) < 8:
                        if create:
                            print 'error: passphrase too short (< 8 chars)'
                    elif len(phrase) < 32: # Pad to 32
                        verify = 1
                    elif len(phrase) == 32:
                        verify = 1
                    elif len(phrase) > 32:
                        if create:
                            print 'warn: passphrase truncated to 32 chars'
                        verify = 1
                else: # Match phrases
                    if not create: break
                    tmp = self._getpass('Re-enter the passphrase:')
                    if tmp == phrase: break
                    else: print 'error: passphrases do not match'; verify=0
        else:
            phrase = use_phrase
        if len(phrase) < 32: # Pad
            phrase = phrase.ljust(32)
        elif len(phrase) > 32: # Truncate to 32 chars
            phrase = phrase[:32]
        md5 = Modules().getinst('md5')            
        phrase_hash = md5.new(phrase + self._salt).hexdigest()
        cipher_key = md5.new(phrase).hexdigest() # -> 32bit key
        return (phrase_hash, cipher_key)

    def _load_file(self):
        if not self._os.path.exists(self._fn): return
        f = open(self._fn, 'r')
        for ln in f:
            ln = ln.rstrip('\n')
            if ln.startswith('#'): continue
            if ln.startswith('passwd_'):
                if ln.startswith('passwd_version'):
                    self._file_version = self._parse_line(ln)[1]
                    # TODO: Version checks here
                elif ln.startswith('passwd_hash'):
                    self._file_hash = self._parse_line(ln)[1]
            else:
                self._entries.append((self._parse_line(ln)))

    def _parse_line(self, ln):
        (name,value) = ln.split('=',1)
        return (name,value.strip('"'))
