#!/usr/bin/env python
#
# Pyring
#
# copyright 2008-2010 Angus Ainslie <angus@akkea.ca>
#
#    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 3 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, see <http://www.gnu.org/licenses/>.
#

import struct
import socket

import pygtk
import gtk
import gobject
import gtk.glade
import hashlib
import Numeric
import pyDes
import binascii
import string
import os
import re
import time
import random
import threading
import sys

from xml.sax import make_parser
from xml.sax import saxutils
from xml.sax import handler
from xml.sax.handler import feature_namespaces

VERSION = "1.2.0"

try:
    import hildon
except:
    hasHildon = False
else:
    hasHildon = True

try :
    # Nasty hack to see if we're running on a Neo Freerunner
    fd = os.open( "/sys/bus/platform/devices/neo1973-pm-bt.0/power_on", os.O_RDWR )
    isNeo = True
except :
    isNeo = False

class ReadPyringXML( handler.ContentHandler ) :

    def __init__(self, version, hash, records ) :
        self.version, self.hash, self.records = version, hash, records
        self.recordsContent = 0
        self.recordContent = 0
        self.versionContent = 0
        self.hashContent = 0
        self.nameContent = 0
        self.dataContent = 0
        #print "Init pyring XML"


    def startElement( self, name, attrs ) :

        if name == 'version' :
            self.versionContent = 1
        elif name == 'hash' :
            self.hashContent = 1
            #self.hash = []
        elif name == 'records':
            self.recordsContent = 1
        elif name == 'record':
            self.record = []
            self.recordContent = 1
        elif name == 'name' :
            if not self.recordContent :
                print "Badly formed XML missing record tag"

            self.nameContent = 1
            self.name = ''
        elif name == 'data' :
            if not self.recordContent :
                print "Badly formed XML missing record tag"

            self.dataContent = 1
            self.data = ''
        else :
            print "Unknown tag <", name, ">"

    def endElement( self, name ) :
        if name == 'version' :
            self.versionContent = 0
        elif name == 'hash' :
            self.hashContent = 0
        elif name == 'records' :
            self.recordsContent = 0
        elif name == 'record':
            if self.nameContent :
                print "Badly formed XML missing name tag"

            if self.dataContent :
                print "Badly formed XML missing data tag"

            self.record = [ self.name, self.data ]
            self.records.append( self.record )
            self.recordContent = 0
        elif name == 'name' :
            self.nameContent = 0
            self.name = binascii.a2b_hex( self.name )
        elif name == 'data' :
            self.dataContent = 0
            self.data = binascii.a2b_hex( self.data )
        else :
            print "Unknown tag ", name

    def characters( self, ch ) :
        if self.hashContent :
            self.hash.append( binascii.a2b_hex( ch ))
        if self.versionContent :
            self.version.append( ch )
        if self.nameContent :
            self.name = self.name +ch
        if self.dataContent :
            self.data = self.data +ch
        #print "ch : ", ch.encode( "hex" )

    def error(self, exception):
        import sys
        sys.stderr.write("\%s\n" % exception)

class Main:
    def __init__(self):
        # do some initalization
        print "Pyring version", VERSION
        print "copyright 2008-2009 Angus Ainslie <angus@akkea.ca>"
        print "mods by Wouter Batelaan $Rev: 233 $ $LastChangedDate: 2009-09-08 23:46:17 +0100 (Tue, 08 Sep 2009) $"

        if hasHildon :
            print "Using hildon UI"
        elif isNeo :
            print "Using Neo UI"
        else :
            print "Using standard UI"

        if isNeo :
            self.gladeFile = "/usr/lib/pyring/neo_gui.glade"
        else :
            self.gladeFile = "/usr/local/lib/pyring/gui.glade"
#            self.gladeFile = "./neo_gui.glade"


        self.wTree = gtk.glade.XML( self.gladeFile, "mainwindow" )

        signals = {
            "on_quit_activate" : self.OnQuit,
            "on_save_activate" : self.OnSave,
            "on_delete_activate" : self.OnRowDelete,
            "on_new_activate" : self.OnRowNew,
            "on_about_activate" : self.OnAbout,
            "on_about_close" : self.OnAboutClose,
            "on_change_password_activate" : self.OnChangePassword,
            "on_mainwindow_delete_event" : self.OnDestroy,
            "on_mainwindow_event" : self.OnResetPasswordExpire,
            "on_import_activate" : self.OnImport,
            "on_dedup_activate" : self.OnDedup,
            "on_nameView_row_activated" : self.OnRowActivated,
            "on_nameView_move_cursor" : self.OnCursorMoved,
            "on_nameView_select_cursor_row" : self.OnRowActivated,
            "on_nameView_sort" : self.RecordSort,
            "on_name_text_changed" : self.OnNameTextChanged,
            "on_category_text_changed" : self.OnCategoryTextChanged,
            "on_account_text_changed" : self.OnAccountTextChanged,
            "on_password_text_changed" : self.OnPasswordTextChanged,
            "on_note_text_changed" : self.OnNoteTextChanged,
            "on_main_genpwd_btn_clicked" : self.OnMainGenpwdBtn,
            "on_main_update_btn_activate" : self.OnMainUpdateBtn,
            "on_main_cancel_btn_activate" : self.OnMainCancelBtn,
            "on_categories_changed" : self.OnCategoriesChanged,
            "on_category_visible_changed" : self.OnCategoryVisibleChanged,
        }

        self.wTree.signal_autoconnect(signals)

        #Here are some variables that can be reused later
        self.saltSize = 4
        self.salt = None
        self.hashSize = 16
        self.cName = 1
        self.cCategory = 0
        self.hasChanged = False
        self.passwordExpire = 0
        self.passwordExpireSuspend = False
        self.resetExpire = 30 # multiple of 10 secs

        self.sName = "Name"
        self.sCategory = "Category"

        if hasHildon :
            self.confDir = os.path.expanduser( '~/MyDocs/.pyring' )
        else :
            self.confDir = os.path.expanduser( '~/.pyring' )

#        self.confFile = os.path.join( self.confDir, "pyring.conf" )
        self.dataFile = os.path.join( self.confDir, "pyringDB.xml" )

        self.pwDigest = None
        self.hash = ''
        self.records = []
        self.curRecord = None
        self.recordChanged = 0
        self.categories = set()
        self.category = "*"
        self.disableCategoryUpdate = False

        self.parseArgs()

        if not os.path.isdir( self.confDir ) :
            print "Creating config directory :", self.confDir
            os.mkdir( self.confDir )

#        if not os.path.isfile( self.confFile ) :
#            print "Creating config file :", self.confFile
#            f = open( self.confFile, 'wb' )
#            f.write( "config file\n" )
#            f.close


        #Get the treeView from the widget Tree
        self.nameView = self.wTree.get_widget("nameView")

        self.AddListColumn(self.sCategory, self.cCategory, False)
        self.AddListColumn(self.sName, self.cName, True)

        self.nameList = gtk.ListStore(str, str, int)
#        self.nameList = gtk.ListStore(str, int)
        self.nameList.connect("sort-column-changed", self.RecordSort)
        self.nameView.set_model(self.nameList)
        self.nameView.set_search_equal_func(self.NameSearchFunc)

        self.categoriesCombo = self.wTree.get_widget("categories")
        self.categoryVisibleCombo = self.wTree.get_widget("category_visible")

        if hasHildon == False :
            self.window = self.wTree.get_widget("mainwindow")
        else:
            self.app = hildon.Program()
            vMain = self.wTree.get_widget("vMain")
            self.window = hildon.Window()
            self.window.set_title('Pyring')
            self.window.connect("destroy", self.OnQuit)
            self.app.add_window(self.window)

            vMain.reparent(self.window)

            menu = gtk.Menu()
            mainMenu = self.wTree.get_widget("mainmenu")
            for child in mainMenu.get_children():
                child.reparent(menu)
            self.window.set_menu(menu)

            mainMenu.destroy()

        self.window.show_all()

        # the list needs to be realized before it can be sorted
        if os.path.exists( self.dataFile ) :
            self.readPyringDB( self.dataFile )
            if "-csv-export" in sys.argv:
                self.WriteCSV(self.csvFile)
                sys.exit(0)
            if "-merge" in sys.argv:
                self.readPyringDB(self.mergeFile)
            self.RecordSort( self.nameList )
            self.UpdateCategories()
            self.SetCategoriesCombo( self.categoriesCombo )
            self.SetCategoriesCombo( self.categoryVisibleCombo )
            for i,record in enumerate(self.records):
                ( name, category ) = self.SplitName( record[0] )
                self.nameList.append( [category, name, i] )

        self.timer = None
        self.tick()
        if "-dedup" in sys.argv:
            self.OnDedup(None)

    @staticmethod
    def NameSearchFunc(model, column, key, iter):
        if model[iter][0].lower().find(key.lower()) >= 0:
            return False
        else:
            return True

    def parseArgs(self):
        try:
            i = 1
            while i < len(sys.argv):
                arg = sys.argv[i]
                if arg.startswith("-"):
                    if arg == "-merge":
                        i += 1
                        self.mergeFile = sys.argv[i]
                    elif arg == "-csv-export":
                        i += 1
                        self.csvFile = sys.argv[i]
                    elif arg == "-dedup":
                        pass
                    else:
                        raise Exception("Unknown option: " + arg)
                else:
                    print "Using data file", arg
                    self.dataFile = arg
                i += 1
        except BaseException, e:
            print e
            self.PrintUsage()
            sys.exit(1)

    def PrintUsage(self):
        print '''
Usage: pyring [<options> [<dataFile>]
Options:
  -merge <mergeFile>    - read in additional data file
  -dedup                - run de-duplication function on startup
  -csv-export <csvFile> - export to csv file on startup, and exit.
'''
    def PasswordExpired( self ) :
        self.ClearFields()
        self.ClearButtons()
        # Trash changes on password expire
        self.recordChanged = False

        return False

    def tick( self ):
        if self.passwordExpireSuspend:
            return
        if self.passwordExpire == 0 :
            if self.pwDigest != None :
                self.pwDigest = None
                gobject.idle_add( self.PasswordExpired )

        if self.passwordExpire > 0 :
            if self.timer != None :
                self.timer.cancel()
            self.timer = threading.Timer(10.0, self.tick)
            self.timer.start()
            self.passwordExpire -= 1

        if self.passwordExpire < 0 :
            self.passwordExpire = 0


    def OnResetPasswordExpire( self, one, two ) :
        self.ResetPasswordExpire()

    def ResetPasswordExpire( self ) :
        #print "password timer reset :", self.passwordExpire
        if self.passwordExpire == 0 :
           self.timer = threading.Timer(10.0, self.tick)
           self.timer.start()

        self.passwordExpire = self.resetExpire

    def OnChangePassword(self, widget ):
        self.ResetPasswordExpire()
        password = self.GetPassword( "Old Password" )
        if self.pwDigest == None :
            if not self.CheckPalmKeyringPasswd( password, True ) :
                return False
        else :
            if not self.CheckPalmKeyringPasswd( password, False ) :
                return False

        password1 = self.GetPassword( "New Password" )
        password2 = self.GetPassword( "Again" )

        if password1 != password2 :
            self.show_error_dialog("New passords don't match" )
            return False

        password.zfill( len( password ))
        password2.zfill( len( password2 ))

        self.hash = HashPassword( self.salt, password1 )
        newDigest = DigestPassword( password1 )

        password1.zfill( len( password1 ))

        self.records = self.reHash( self.records, self.pwDigest, newDigest )
        self.pwDigest = newDigest
        self.hasChanged = True

        return True

    def DigestPassword( self, password ):
        m = hashlib.md5( password )
        return m.digest()

    def HashPassword( self, salt, password ):
        m = hashlib.md5()
        m.update( salt )
        m.update( password )
        # pad with zeros
        m.update( ''.rjust( 64 - len( salt ) - len( password ), '\0' ) )

        return m.digest()

    def reHash( self, records, oldDigest, newDigest ):
        record = ''

        dialog = gtk.Dialog("Progress", None, 0 )

        box = dialog.get_child()

        progress = gtk.ProgressBar()
        box.pack_start(progress, False, False, 0)
        progress.set_fraction(0.0)

        dialog.show_all()

        num_records = len( records );

        try:
            self.passwordExpireSuspend = True
            for i in range( num_records ) :
                record = self.DecodeRecord( records[i][1], oldDigest )
                record = string.join( record, '\0' )
                records[i][1] = self.EncodeRecord( record, newDigest )
                progress.set_fraction( i/(num_records + 1.0))
                while gtk.events_pending():
                    gtk.main_iteration_do(False)
        finally:
            self.passwordExpireSuspend = False

        dialog.destroy()

        self.hasChanged = True;

        return records

    def OnImport(self, widget ):
        if hasHildon :
            dialog = hildon.FileChooserDialog(self.window,
                                           gtk.FILE_CHOOSER_ACTION_OPEN);
        else :
            dialog = gtk.FileChooserDialog("Import ..",
                                       None,
                                       gtk.FILE_CHOOSER_ACTION_OPEN,
                                       (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                       gtk.STOCK_OPEN, gtk.RESPONSE_OK))
        dialog.set_default_response(gtk.RESPONSE_OK)

        filter = gtk.FileFilter()
        filter.set_name("CSV File")
        filter.add_pattern("*.csv")
        dialog.add_filter(filter)
        filter = gtk.FileFilter()
        filter.set_name("Pyring XML File")
        filter.add_pattern("*.xml")
        dialog.add_filter(filter)
        filter = gtk.FileFilter()
        filter.set_name("Palm Database File")
        filter.add_pattern("*.pdb")
        dialog.add_filter(filter)

        dialog.show()
        response = dialog.run()
        if response == gtk.RESPONSE_OK:
            name = dialog.get_filename()
            if name != None :
                print name

            if name[-4:] == ".xml" :
                print "XML file"

                dialog.destroy()
                return self.importPyringDB( name )
            elif name[-4:] == ".csv" :
                dialog.destroy()
                return self.ReadCSV(name)
            else :
                keyFile = open( name, "rb")
                dialog.destroy()
                self.hasChanged = True

                ( name, dbAttr, version, crDate, modDate, modNumber,
                  appInfoID, sortInfoID, dbType, creatID, idSeed,
                  nextRecordListID, numRecords,
                  recDirOffset ) = self.ReadPalmDBHeader( keyFile )

                if dbType == 'Gkyr' and creatID == 'Gtkr' :
                    return self.ReadKeyringDB( keyFile )
                elif dbType == 'DATA' and creatID == 'memo' :
                    return self.ReadCryptinfoMemo( keyFile )

                return False

        elif response == gtk.RESPONSE_CANCEL:
            dialog.destroy()
            return False

    def AlphaOnly( self, text ) :
        """list views have a funny way of sorting on only the alpha
        chars in the list - This tries to do the same thing"""
        ret = ''
        a = []

        r = re.compile(r'\w+')
        a = r.findall( str( text ).lower() )

        ret = string.join(a, '' )

        return ret

    def RecordSort( self, list ):
        col = self.nameView.get_column( 1 )
        order = col.get_sort_order()

        if order == gtk.SORT_ASCENDING :
            self.records.sort( self.backward )
        else:
            self.records.sort( self.forward )

    def UpdateCategories( self ):
        if self.disableCategoryUpdate:
            return
        self.disableCategoryUpdate = True
        self.categories.clear()
        for record in self.records:
            ( name, category ) = self.SplitName( record[0] )
            if category != '' :
                self.categories.add( category )
        self.disableCategoryUpdate = False

    def SetCategoriesCombo( self, combo ):
        if self.disableCategoryUpdate:
            return
        
        combo.get_model().clear()
        combo.append_text("*")
        for category in self.categories:
            combo.append_text( category )
        combo.set_active(0)


    def forward( self, a, b, ) :
        strip_a = self.AlphaOnly( a )
        strip_b = self.AlphaOnly( b )
        return cmp( strip_a, strip_b )

    def backward( self, a, b ) :
        strip_a = self.AlphaOnly( a )
        strip_b = self.AlphaOnly( b )
        return cmp( strip_b, strip_a )

    def importPyringDB( self, fileName ) :
        if not self.CheckPassword() :
            return

        f = open( fileName, "rb")

        if f == None :
            return

        records = []
        versionBytes = []
        hashBytes = []
        dh = ReadPyringXML( versionBytes, hashBytes, records )

        parser = make_parser()

        parser.setFeature(feature_namespaces, 0)
        parser.setContentHandler(dh)

        parser.parse( f )

        salt = hashBytes[0][0:self.saltSize]
        hash = hashBytes[0][self.saltSize:]

        f.close()

        if hash != self.hash :
            password = self.GetPassword( 'Import Password' )
            testHash = self.HashPassword( salt, password )
            if testHash != hash :
                show_error_dialog("Incorrect import password" )

        password.zfill( len( password ))

        digest = self.DigestPassword( password )

        records = self.reHash( records, digest, self.pwDigest )

        self.records = self.records + records
        self.hasChanged = True

        self.RenewNameList()

        self.Dedup()

    def readPyringDB( self, fileName ) :
        f = open( fileName, "rb")

        if f == None :
            return

        versionBytes = []
        hashBytes = []
        dh = ReadPyringXML( versionBytes, hashBytes, self.records )

        parser = make_parser()

        parser.setFeature(feature_namespaces, 0)
        parser.setContentHandler(dh)

        parser.parse( f )

        self.salt = hashBytes[0][0:self.saltSize]
        self.hash = hashBytes[0][self.saltSize:]

        f.close()

    def writePyringDB( self, fileName ) :
        if os.path.exists( fileName ) :
            os.rename( fileName, fileName + '.old' )

        if self.records == [] :
            return

        f = open( fileName, "wb")

        f.write( "<records>\n" )
        f.write( "\t<version>" + '1.0' + "</version>\n" )
        str = binascii.b2a_hex( self.salt + self.hash )
        f.write( "\t<hash>" +str + "</hash>\n" )
        for record in self.records :
            f.write( "\t<record>\n" )
            str = binascii.b2a_hex( record[0] )
            f.write( "\t\t<name>" + str + "</name>\n" )
            f.write( "\t\t<data>" )
            str = binascii.b2a_hex( record[1] )
            f.write( str )
            f.write( "</data>\n" )
            f.write( "\t</record>\n" )
        f.write( "</records>\n" )

        f.close()

    def CheckCurrentRow( self, validate ) :
        if self.recordChanged == 1 :
            if not self.CheckPassword() :
                return False

            self.recordChanged = 0
            if self.curRecord != None :
                if validate :
                    save = self.show_warning_dialog( "Apply Changes" )
                    if save == gtk.RESPONSE_NO :
                        return False

                self.UpdateRecordFromRow()
                return True

        return False

    def OnQuit(self,widget) :
        self.ResetPasswordExpire()
        self.CheckCurrentRow( True )

        if self.hasChanged :
            save = self.show_warning_dialog( "Save Changes ?" )
            if save == gtk.RESPONSE_YES :
                self.writePyringDB( self.dataFile )

        if self.pwDigest != None :
            self.pwDigest.zfill( len( self.pwDigest ))

        if self.timer :
            self.timer.cancel()

        gtk.main_quit()

    def OnSave(self,widget) :
        self.ResetPasswordExpire()
        self.ClearButtons()
        self.CheckCurrentRow( False )

        if self.hasChanged :
            self.hasChanged = False
            self.writePyringDB( self.dataFile )

    def OnDestroy(self,widget , arg ) :
        self.OnQuit( widget )

    def OnAboutClose( self, arg ) :
        self.ResetPasswordExpire()
        self.aboutDlg.destroy()

    def OnAbout( self, arg ) :
        self.ResetPasswordExpire()
        aboutTree = gtk.glade.XML( self.gladeFile, "aboutdialog" )

        self.aboutDlg = aboutTree.get_widget("aboutdialog")
        self.aboutDlg.run()
        self.aboutDlg.destroy()

    def OnDedup( self, arg ) :
        self.ResetPasswordExpire()
        self.Dedup()

    def Dedup( self ) :
        if not self.CheckPassword() :
            return 0

        i = 1
        while i < len( self.records ) :
            record1 = self.records[i-1]
            record2 = self.records[i]
            if not cmp( record1[0], record2[0] ) : # name is the same
                if not cmp( record1[1], record2[1] ) : # data is the same, delete record1
                    response = 1
                    self.hasChanged = True
                else:
                    # data is different, compare the records
                    cur = self.nameView.set_cursor( i )
                    response = self.CompareRecords( record1, record2 )

                if response == 1: # del lhs, keep rhs
                    del self.records[i-1]
                    i -= 1
                elif response == -1: # keep lhs, del rhs
                    del self.records[i]
                    i -= 1

            i += 1

        self.RenewNameList()


    def CompareRecords( self, recordL, recordR ) :
        if not self.CheckPassword() :
            return 0

        compareTree = gtk.glade.XML( self.gladeFile, "compare_dlg" )

        compareDlg = compareTree.get_widget("compare_dlg")

        recordL_data = self.DecodeRecord( recordL[1], self.pwDigest )
        recordR_data = self.DecodeRecord( recordR[1], self.pwDigest )

        fld = compareTree.get_widget("nameL")
        fld.set_text( recordL[0] )

        fld = compareTree.get_widget("accountL")
        fld.set_text( recordL_data[0] )

        fld = compareTree.get_widget("passwordL")
        fld.set_text( recordL_data[1] )

        fld = compareTree.get_widget("noteL")
        buf = fld.get_buffer()
        buf.delete( buf.get_start_iter(), buf.get_end_iter() )
        iter = buf.get_start_iter()
        buf.insert( iter, recordL_data[2] )
        fld.set_buffer( buf )

        fld = compareTree.get_widget("nameR")
        fld.set_text( recordR[0] )

        fld = compareTree.get_widget("accountR")
        fld.set_text( recordR_data[0] )

        fld = compareTree.get_widget("passwordR")
        fld.set_text( recordR_data[1] )

        fld = compareTree.get_widget("noteR")
        buf = fld.get_buffer()
        buf.delete( buf.get_start_iter(), buf.get_end_iter() )
        iter = buf.get_start_iter()
        buf.insert( iter, recordR_data[2] )
        fld.set_buffer( buf )

        response = compareDlg.run()

        # update records from editing
        if response != 3:
            self.updateAfterCompare(compareTree, recordL, "L")
        if response != 1:
            self.updateAfterCompare(compareTree, recordR, "R")

        compareDlg.destroy()

        if response == 1:
            return -1

        if response == 2:
            return 0

        if response == 3:
            return 1


    def updateAfterCompare(self, compareTree, record, which):
        fld = compareTree.get_widget("name" + which)
        if fld.get_text() != record[0]:
            print "record ", which, "has changed its name from", record[0], "to", fld.get_text()
            record[0] = fld.get_text()
            self.hasChanged = True
        string = compareTree.get_widget("account" + which).get_text() + \
            '\0' + compareTree.get_widget("password" + which).get_text()
        fld = compareTree.get_widget("note" + which)
        buffer = fld.get_buffer()
        startIter = buffer.get_start_iter()
        endIter = buffer.get_end_iter()
        string = string + '\0' + buffer.get_text( startIter, endIter, True ) + '\0'
        encoding = self.EncodeRecord( string, self.pwDigest )
        if record[1] != encoding:
            print "record ", which, "has changed its data"
            record[1] = encoding
            self.hasChanged = True

    def GetPassword( self, title = 'Password' ) :
        passTree = gtk.glade.XML( self.gladeFile, "passwd_dlg" )

        passwdDlg = passTree.get_widget("passwd_dlg")
        passwdDlg.set_title( title )
        response = passwdDlg.run()
        passwd_entry = passTree.get_widget("passwd_entry")

        password = ''

        if response == gtk.RESPONSE_OK:
            password = passwd_entry.get_text()
            passwdDlg.destroy()
            if not password:
                return None

            passwdDlg.destroy()

        return password

    def TextChanged( self ) :
        self.ResetPasswordExpire()
        self.recordChanged = 1
        self.ActivateButtons()

    def OnNameTextChanged( self, event ) :
        self.TextChanged()

    def OnCategoryTextChanged( self, event ) :
        print "Enter OnCategoryTextChanged" 
        active = self.categoriesCombo.get_active()
        if active < 0:
            category = "*"
        else:
            category = self.categoriesCombo.get_model()[active][0]
        categoryText = self.wTree.get_widget("category_text")
        if( categoryText.get_text() == category ) : 
            print "Early exit OnCategoryTextChanged" 
            return
        self.TextChanged()
        print "Exit OnCategoryTextChanged" 

    def OnAccountTextChanged( self, event ) :
        self.TextChanged()

    def OnPasswordTextChanged( self, event ) :
        self.TextChanged()

    def OnNoteTextChanged( self, one, two ) :
        self.TextChanged()

    def SplitName( self, nameCat ) :
        name = nameCat
        category = ''
        i = nameCat.find("\0")
        if i > 0 and i< len( nameCat ) :
            name = nameCat[0:i]
            category = nameCat[i+1:];

        return ( name, category )

    def OnMainCancelBtn( self, event ) :
        self.ResetPasswordExpire()
        if not self.CheckPassword() :
            return

        record = self.DecodeRecord( self.records[self.curRecord][1]
                                    , self.pwDigest )

        ( name, category ) = self.SplitName( self.records[self.curRecord][0] )  

        self.SetFields( name, category, record[0],
                            record[1], record[2] )
        self.recordChanged = 0
        self.ClearButtons()

    def CheckPassword( self ) :
        if self.pwDigest == None :
            password = self.GetPassword()
            if not self.CheckPalmKeyringPasswd( password ) :
                return False

        return True

    def UpdateRecordFromRow( self ) :
        if self.curRecord == None :
            print "Can't update None "
            return

        self.hasChanged = True
        fld = self.wTree.get_widget("name_text")
        string = fld.get_text()
        fld = self.wTree.get_widget("category_text")
        string = string + '\0' +  fld.get_text()
        self.records[self.curRecord][0] = string

        fld = self.wTree.get_widget("account_text")
        string = fld.get_text()
        fld = self.wTree.get_widget("password_text")
        string = string + '\0' +  fld.get_text()
        fld = self.wTree.get_widget("note_text")
        buffer = fld.get_buffer()
        startIter = buffer.get_start_iter()
        endIter = buffer.get_end_iter()
        string = string +'\0' + buffer.get_text( startIter, endIter, True ) + '\0'
        self.records[self.curRecord][1] = self.EncodeRecord( string, self.pwDigest )
        self.RenewNameList()

    def ClearButtons( self ) :
        updateBtn = self.wTree.get_widget("main_update_btn")
        updateBtn.set_sensitive( False )
        cancelBtn = self.wTree.get_widget("main_cancel_btn")
        cancelBtn.set_sensitive( False )
        pwdBtn = self.wTree.get_widget("main_genpwd_btn")
        pwdBtn.set_sensitive( False )

    def ActivateButtons( self ) :
        updateBtn = self.wTree.get_widget("main_update_btn")
        updateBtn.set_sensitive( True )
        cancelBtn = self.wTree.get_widget("main_cancel_btn")
        cancelBtn.set_sensitive( True )
        pwdBtn = self.wTree.get_widget("main_genpwd_btn")
        pwdBtn.set_sensitive( True )

    def DeactivateFields( self ) :
        fld = self.wTree.get_widget("name_text")
        fld.set_sensitive( False )
        fld = self.wTree.get_widget("category_text")
        fld.set_sensitive( False )
        fld = self.wTree.get_widget("account_text")
        fld.set_sensitive( False )
        fld = self.wTree.get_widget("password_text")
        fld.set_sensitive( False )
        fld = self.wTree.get_widget("note_text")
        fld.set_sensitive( False )

    def ActivateFields( self ) :
        fld = self.wTree.get_widget("name_text")
        fld.set_sensitive( True )
        fld = self.wTree.get_widget("category_text")
        fld.set_sensitive( True )
        fld = self.wTree.get_widget("account_text")
        fld.set_sensitive( True )
        fld = self.wTree.get_widget("password_text")
        fld.set_sensitive( True )
        fld = self.wTree.get_widget("note_text")
        fld.set_sensitive( True )

    def SetFields( self, name, category, account, password, note ) :
        if name == None :
            name = ''
        fld = self.wTree.get_widget("name_text")
        fld.set_text( name )
        if category == None or category == "*":
            category = ''
        fld = self.wTree.get_widget("category_text")
        fld.set_text( category )
        if password == None :
            password = ''
        fld = self.wTree.get_widget("account_text")
        fld.set_text( account )
        if password == None :
            password = ''
        fld = self.wTree.get_widget("password_text")
        fld.set_text( password )
        if note == None :
            note = ''
        fld = self.wTree.get_widget("note_text")
        buf = fld.get_buffer()
        buf.delete( buf.get_start_iter(), buf.get_end_iter() )
        iter = buf.get_start_iter()
        if note == None :
            buf.insert( iter, note )
        else :
            buf.insert( iter, '' )            
        fld.set_buffer( buf )

    def ClearFields( self ) :
        self.SetFields( '', '', '', '', '' )

    def OnRowNew( self, one ) :
        self.ResetPasswordExpire()
        if not self.CheckPassword() :
            return

        self.CheckCurrentRow( True )

        sel = self.nameView.get_selection()

        selected = sel.get_selected()
        liststore = selected[0]

        iter = liststore.insert( 0 )
        self.records.insert( 0, ['','',''] )

        self.curRecord = 0

        path = liststore.get_path( iter )

        self.nameView.set_cursor_on_cell( path )

        cur = self.nameView.get_cursor()
        self.curRecord = cur[0][0]

        self.ClearButtons()
        self.ClearFields()
        self.ActivateFields()
        pwdBtn = self.wTree.get_widget("main_genpwd_btn")
        pwdBtn.set_sensitive( True )
        self.recordChanged = 1

    def OnRowDelete( self, one ) :
        self.ResetPasswordExpire()
        if not self.CheckPassword() :
            return

        if self.curRecord == None :
            return

        sel = self.nameView.get_selection()

        selected = sel.get_selected()
        liststore = selected[0]
        iter = selected[1]

        if self.recordChanged == 1 :
            self.recordChanged = 0
            self.ClearButtons()

        if self.curRecord != None :
            delete = self.show_warning_dialog( "Delete Row" )
            if delete == gtk.RESPONSE_YES :
                self.hasChanged = True
                del self.records[self.curRecord]
                liststore.remove( iter )
                self.ClearFields()
                self.recordChanged = 0

    def OnMainGenpwdBtn(self, event):
        self.ResetPasswordExpire()
        genpwdTree = gtk.glade.XML(self.gladeFile, "genpwd_dlg")
        self.genpwdDlg = genpwdTree.get_widget("genpwd_dlg")

        if self.genpwdDlg.run() == gtk.RESPONSE_OK:
	    pwdlen = self.GetPwdLen(genpwdTree)
	    pwdchars = self.GetPwdChars(genpwdTree)
            self.Genpwd(pwdlen, pwdchars)
        self.genpwdDlg.destroy()

    def GetPwdLen (self, tree):
	if tree.get_widget("pwdlen_4").get_active():
	    return 4
	elif tree.get_widget("pwdlen_6").get_active():
	    return 6
	elif tree.get_widget("pwdlen_8").get_active():
	    return 8
	elif tree.get_widget("pwdlen_10").get_active():
	    return 10
        elif tree.get_widget("pwdlen_16").get_active():
            return 16
	elif tree.get_widget("pwdlen_20").get_active():
	    return 20

    def GetPwdChars (self, tree):
	chars = ''
	if tree.get_widget("pwdinc_lowercase").get_active():
	    chars += string.lowercase
	if tree.get_widget("pwdinc_uppercase").get_active():
	    chars += string.uppercase
	if tree.get_widget("pwdinc_digits").get_active():
	    chars += string.digits
	if tree.get_widget("pwdinc_punctuation").get_active():
	    chars += string.punctuation
	return chars

    def Genpwd(self, length, chars):
	"""Generate a password of length characters, using characters
	in chars."""
        tree = self.wTree
        fld = tree.get_widget('password_text')
	if chars == '':
	    return
	# Create a new seed every time.  Keyring uses 256 bytes
	random.seed(os.urandom(256))
	password = ''.join([random.choice(chars) for i in range(length)])
	fld.set_text(password)

    def OnMainUpdateBtn( self, event ) :
        self.ResetPasswordExpire()
        self.ClearButtons()

        if self.recordChanged == 1 :
            self.recordChanged = 0
            if self.curRecord != None :
                self.disableCategoryUpdate = True 
                self.UpdateRecordFromRow()
                self.disableCategoryUpdate = False 
                ( name , category ) = self.SplitName( self.records[self.curRecord][0] )
                self.SetListItem( self.curRecord, name, category )

    def SetListItem( self, row, name, category ) :
        iter = self.nameList.get_iter( (row,) )
        self.nameList.set( iter, 0, category )
        self.nameList.set( iter, 1, name )

    def OnCursorMoved( self, tree, event, direction ) :
        self.ResetPasswordExpire()
        cur = self.nameView.get_cursor()
        if cur == None :
            return

        recordNum = cur[0][0]
        recordNum += direction

        if recordNum < 0 :
            recordNum = 0

        if recordNum == len( self.records ) :
            recordNum = len( self.records ) - 1

        self.DisplayRecord( recordNum )


    def OnRowActivated( self, tree, event ) :
        self.ResetPasswordExpire()
        sel = self.nameView.get_selection()
        (model,iter) = self.nameView.get_selection().get_selected()
        if iter == None: return
        self.DisplayRecord( model[iter][2] )

    def DisplayRecord( self, recordNum ) :
        if self.recordChanged == 1 :
            self.recordChanged = 0
            self.ClearButtons()
            if self.curRecord != None :
                save = self.show_warning_dialog( "Apply Changes ?" )
                if save == gtk.RESPONSE_YES :
                    self.UpdateRecordFromRow()
                    ( name, category ) = self.SplitName( self.records[self.curRecord][0] )
                    self.SetListItem( self.curRecord, name, category )

        if not self.CheckPassword() :
            return

        record = self.DecodeRecord( self.records[recordNum][1],
                                    self.pwDigest )

        ( name, category ) = self.SplitName( self.records[recordNum][0] )
        self.SetFields( name, category, record[0], record[1], record[2] )
        self.recordChanged = 0
        self.ClearButtons()
        self.ActivateFields()
        pwdBtn = self.wTree.get_widget("main_genpwd_btn")
        pwdBtn.set_sensitive( True )
        self.curRecord = recordNum

    def OnSelectRow( self, editing, user_param ) :
        self.ResetPasswordExpire()
        cur = self.nameView.get_cursor()

    def OnCategoriesChanged(self, user_param1):
        print "Enter OnCategoriesChanged" 
        if self.disableCategoryUpdate: 
            print "Early exit OnCategoriesChanged" 
            return
        active = self.categoriesCombo.get_active()
        if active < 0:
            category = "*"
        else:
            category = self.categoriesCombo.get_model()[active][0]
        self.TextChanged()
        self.disableCategoryUpdate = True
        fld = self.wTree.get_widget("category_text")
        fld.set_text( category )
        self.disableCategoryUpdate = False
        print "Exit OnCategoriesChanged" 

    def OnCategoryVisibleChanged(self, user_param1):
        if self.disableCategoryUpdate: 
            return
        active = self.categoryVisibleCombo.get_active()
        if active < 0:
            self.category = "*"
        else:
            self.category = self.categoryVisibleCombo.get_model()[active][0]
        self.disableCategoryUpdate = True
        self.RenewNameList()
        self.disableCategoryUpdate = False

    def AddListColumn(self, title, columnId, sort):
        """This function adds a column to the list view.
        First it create the gtk.TreeViewColumn and then set
        some needed properties"""

        column = gtk.TreeViewColumn(title, gtk.CellRendererText()
                                    , text=columnId)
        column.set_resizable(True)
        if sort :
            column.set_sort_order( gtk.SORT_DESCENDING )
            column.set_sort_column_id(columnId)
        else :
            column.set_clickable( False )

        self.nameView.append_column(column)

    def RecordString(self, record ) :
        for i in range( 0, len(record ) - 1 ) :
            if record[i] == '\0' :
                return record[:i]

        return record

    def ReadCSV(self, filename ) :
        import csv
        if not self.CheckPassword() :
            return False
        reader = csv.reader(open(filename))
        i=0
        for row in reader:
            name = row[0]
            account = row[1]
            password = row[2]
            if len(row) >= 4:
                note = row[3]
            else:
                note = ''
            i+=1
            print i, 'Name:', name, 'Account:', account, 'Password:', password, 'Note:', note
            recordData = '\0'.join( [ account, password, note ] )
            recordData = ''.join( [ recordData, '\0' ] )
            enc = self.EncodeRecord( recordData, self.pwDigest )
            record = [ name, enc ]
            self.records.append( record )
            self.hasChanged = True
        self.RenewNameList()
        return True

    def WriteCSV(self, filename ) :
        import csv
        if not self.CheckPassword() :
            return False
        fp = open(filename, "w")
        writer = csv.writer(fp)
        i=0
        for record in self.records:
            recordDecoded = self.DecodeRecord(record[1], self.pwDigest)
            fields = recordDecoded[2].split('\0')
            row = [record[0], recordDecoded[0], recordDecoded[1]] + fields
            i+=1
            #print i, row
            writer.writerow(row)
        fp.close()
        return True

    def readAlpha(self, f,n):
        retVal = f.read(n)
        return retVal

    def readShort(self,f):
        """Read unsigned 2 byte value from a file f."""
        (retVal,) = struct.unpack("H", f.read(2))
        return socket.ntohs( retVal )

    def readLong(self,f):
        """Read unsigned 4 byte value from a file f."""
        (retVal,) = struct.unpack("I", f.read(4))
        return socket.ntohl( retVal )


    def ReadPalmDBHeader(self, f ) :
        f.seek(0)
        name = self.readAlpha(f,32)

        dbAttr = self.readShort(f)
        version = self.readShort(f)

        crDate = self.readLong(f)
        modDate = self.readLong(f)
        backupDate = self.readLong(f)
        modNumber = self.readLong(f)
        appInfoID = self.readLong(f)
        sortInfoID = self.readLong(f)
        dbType = self.readAlpha(f,4)
        creatID = self.readAlpha(f,4)
        idSeed = self.readLong(f)
        nextRecordListID = self.readLong(f)
        numRecords = self.readShort(f)
        recDirOffset = f.tell()

        return ( name, dbAttr, version, crDate, modDate, modNumber, appInfoID,
                 sortInfoID, dbType, creatID, idSeed, nextRecordListID,
                 numRecords, recDirOffset )

    def ReadKeyringHeader(self, f ) :
        ( name, dbAttr, version, crDate, modDate, modNumber, appInfoID,
          sortInfoID, dbType, creatID, idSeed, nextRecordListID,
          numRecords, recDirOffset ) = self.ReadPalmDBHeader( f )

        if version != 4 :
            self.show_error_dialog("This hasn't been tested with pdb versions other than 4" )

        f.seek( recDirOffset )
        offset = self.readLong(f)
        f.seek( offset )
        salt = f.read( self.saltSize )
        hash = f.read( self.hashSize )

        return ( name, dbAttr, version, crDate, modDate, modNumber, appInfoID,
          sortInfoID, dbType, creatID, idSeed, nextRecordListID,
          numRecords, recDirOffset, salt, hash )

    def CheckPalmKeyringPasswd( self, password, updateDigest = True ) :
        # we've got no salt so we must be creating a new database
        # need to generate a hash too
        if self.salt == None :
            print "no salt - generating some"
            random.seed()
            self.salt = ''
            salt = random.getrandbits( self.saltSize*8 )
            for i in range( self.saltSize ) :
                self.salt = self.salt + chr( salt >> ( self.saltSize - i ) & 0xFF )
#            print "salt:", str( self.salt )
            m = hashlib.md5()
            m.update( self.salt )
            m.update( password )
            # pad with zeros
            m.update(''.rjust(64 - len( self.salt ) - len( password ), '\0' ) )
            self.hash = m.digest()


        m = hashlib.md5()
        m.update( self.salt )
        m.update( password )
        # pad with zeros
        m.update( ''.rjust( 64 - len( self.salt ) - len( password ), '\0' ) )

        digest = m.digest()

        # Only check the hash if there is already data
        if len( self.records ) != 0 :
            for i in range( 1 , len( self.hash )):
                if digest[i] != self.hash[i] :
                    self.show_error_dialog("Wrong password")
                    return False

        if updateDigest  :
            m = hashlib.md5( password )
            self.pwDigest = m.digest()

        password.zfill( len( password ))

        return True

    def ReadCryptinfoMemo(self, f ) :
        ( name, dbAttr, version, crDate, modDate, modNumber, appInfoID,
          sortInfoID, dbType, creatID, idSeed, nextRecordListID,
          numDBRecords, recDirOffset ) = self.ReadPalmDBHeader( f )

        if not self.CheckPassword() :
            return False

        startRecord = False
        startNote = False

        for i in range ( 0, numDBRecords-1 ) :
            f.seek( i*8 + recDirOffset )
            recordOffset = self.readLong(f)
            recordAttr = f.read(1)
            recordID = f.read(3)
            #if not ( recordAttr[0] & 0x08 ):
            nextOffset = self.readLong(f)
            recordLen = nextOffset - recordOffset;
            f.seek( recordOffset )
            while recordLen :
                line = f.readline( recordLen )
                if line == None :
                    recordLen = 0;
                elif startRecord == False and line.startswith( "Title: " ) :
                    startRecord = True
                    name = line[len( "Title: " ):].strip()

                    note = ''
                    account = ''
                    passwd = ''
                elif startRecord == True :
                    if line.startswith( "Note:" ) :
                        startNote = True
                        note += line[len( "Note:" ):].lstrip()
                    elif startNote == False and line.startswith( "Login:" ) :
                        account = line[len( "Login:" ):].strip()
                    elif startNote == False and line.startswith( "URL:" ) :
                        if len( line[len( "URL:" ):].strip() ) != 0 :
                            note += line.strip()
                    elif startNote == False and line.startswith( "Pwd:" ) :
                        password = line[len( "Pwd:" ):].strip()
                    elif startNote == True and line.startswith( "\n" ) :
                        startRecord = False
                        startNote = False
                        recordData = '\0'.join( [ account, password, note ] )
                        recordData = ''.join( [ recordData, '\0' ] )
                        enc = self.EncodeRecord( recordData, self.pwDigest )
                        record = [ name, enc ]
                        self.records.append( record )
                    else:
                        note += line.strip() + '\n'

                recordLen = recordLen - len( line )

        self.RenewNameList()

    def RenewNameList( self ) :
        self.nameList.clear()

        self.RecordSort( self.nameList )
        self.UpdateCategories()
        self.SetCategoriesCombo( self.categoriesCombo )
        self.SetCategoriesCombo( self.categoryVisibleCombo )
        for i,record in enumerate(self.records):
            ( name, category ) = self.SplitName( record[0] )
            if self.category == "*" or category == self.category:
                self.nameList.append( [ category, name, i ] )

    def ReadKeyringDB(self, f ) :
        self.pwDigest = None

        ( name, dbAttr, version, crDate, modDate, modNumber, appInfoID,
          sortInfoID, dbType, creatID, idSeed, nextRecordListID,
          numRecords, recDirOffset, salt, hash ) = self.ReadKeyringHeader( f )

        self.salt = salt
        self.hash = hash

        # self.records = []
        # this skips the last record
        # this should not skip the first record
        # it should just skip the hidden record
        for i in range ( 1, numRecords-1 ) :
            f.seek( i*8 + recDirOffset )
            recordOffset = self.readLong(f)
            recordAttr = f.read(1)
            recordID = f.read(3)
            #if not ( recordAttr[0] & 0x08 ):
            nextOffset = self.readLong(f)
            recordLen = nextOffset - recordOffset;
            self.records.append( self.ReadRecord( f, recordOffset, recordLen ))

        f.seek(( numRecords-1 )*8  + recDirOffset )
        recordOffset = self.readLong(f)
        recordAttr = f.read(1)

        #if not ( recordAttr[0] & 0x08 ):
        f.seek( 0, 2 )
        fileEnd = f.tell()
        recordLen = fileEnd - recordOffset
        self.records.append( self.ReadRecord( f, recordOffset, recordLen ))

#        self.numRecords += numRecords

        self.RenewNameList()

    def ReadRecord( self, f, offset, length ):
        f.seek( offset )
        record = f.read( length )
        recordName = self.RecordString( record )
        recordData = record[ len( recordName ) + 1:length]
        return [ recordName, recordData ]

    def DecodeRecord( self, enc, digest ):

        k = pyDes.triple_des( digest, pyDes.ECB )
        mod = len( enc ) % 8

        if mod != 0 :
            enc = enc + ''.rjust( 8 - mod, '\0' )

        dec = k.decrypt( enc )

        ret = []
        ret.append( self.RecordString( dec ))
        dec = dec[ len( ret[0] ) + 1: len(dec) ]
        ret.append( self.RecordString( dec ))
        dec = dec[ len( ret[1] ) + 1: len(dec) ]
        ret.append( self.RecordString( dec ))

        return ret

    def EncodeRecord( self, dec, digest ):

        k = pyDes.triple_des( digest, pyDes.ECB )
        mod = len( dec ) % 8

        # should add random data instead
        if mod != 0 :
            dec = dec + ''.rjust( 8 - mod, '\0' )

        enc = k.encrypt( dec )

        return enc

    def show_error_dialog(self, error_string):
	"""This Function is used to show an error dialog when an error occurs.
	error_string - The error string that will be displayed on the dialog.
	"""
	error_dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR
				, message_format=error_string
				, buttons=gtk.BUTTONS_OK)
	error_dlg.run()
	error_dlg.destroy()

    def show_warning_dialog(self, msg_string):
	"""This Function is used to show an error dialog when an error occurs.
	msg_string - The error string that will be displayed on the dialog.
	"""
	msg_dlg = gtk.MessageDialog(type=gtk.MESSAGE_WARNING
				, message_format=msg_string
				, buttons=gtk.BUTTONS_YES_NO )
	ret = msg_dlg.run()
	msg_dlg.destroy()
        return ret

gtk.gdk.threads_init()
gtk.gdk.threads_enter()

try:
    start = Main()
except BaseException, ex:
    print ex
    sys.exit(1)
#gobject.timeout_add( start, 10, start.tick )

gtk.main()
gtk.gdk.threads_leave()
