#!/usr/bin/env python
#
# Pyring 
#
# copyright 2008-2009 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

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.1.10"

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 verison", VERSION
        print "copyright 2008-2009 Angus Ainslie <angus@akkea.ca>"

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

        if isNeo :
            self.gladeFile = "/usr/share/pyring/neo_gui.glade"
        else :
            self.gladeFile = "/usr/local/lib/pyring/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_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,
        }

        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 = 0
        self.cUsername = 1
        self.hasChanged = False
        self.passwordExpire = 0
        self.resetExpire = 30

        self.sName = "Name"
        self.sUsername = "Username"

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

 #       self.confDir = os.path.join( self.userDir, '.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.recordChanged = 0

        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.sName, self.cName, True)
#        self.AddListColumn(self.sUsername, self.cUsername, False)

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

        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 )
            self.RecordSort( self.nameList )
            for record in self.records : 
                self.nameList.append( [record[0], ''] )

        self.timer = None
        self.tick()

    def PasswordExpired( self ) :
        self.ClearFields()
        self.ClearButtons()
        # Trash changes on password expire
        self.recordChanged = False

        return False

    def tick( self ):
        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 : 
            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 );

        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 )
            # this takes a long time so make sure the password doesn't expire
            self.passwordExpire = self.resetExpire
            progress.set_fraction( i/(num_records + 1.0))
            while gtk.events_pending():
                gtk.main_iteration_do(False)

        dialog.destroy()

        self.hasChanged = True;

        return records

    def OnImport(self, widget ):
        self.ResetPasswordExpire()
        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("Palm Database File")
        filter.add_pattern("*.pdb")

        dialog.add_filter(filter)

        filter = gtk.FileFilter()
        filter.set_name("Pyring XML")
        filter.add_pattern("*.xml")

        dialog.add_filter(filter)

        dialog.show()
#        while 1:
        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()
                    self.importPyringDB( name )
                else:
                    print "Palm database file"
                    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
                
            else:
                dialog.destroy()
                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( 0 )
        order = col.get_sort_order()

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

    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.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_CANCEL :
                        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_OK :
                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

        old_record = None
        i = 1
        while i < len( self.records ) :
            record1 = self.records[i-1]
            record2 = self.records[i]
            
            if not cmp( record1[0], record2[0] ) :
                if not cmp( record1[1], record2[1] ) :
                    del self.records[i]
                    i -= 1
                else :
                    cur = self.nameView.set_cursor( i )
                    response = self.CompareRecords( record1, record2 )
                    
                    if response == 1 :
                        del self.records[i-1]
                        i -= 1
                    elif response == -1:
                        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()

        compareDlg.destroy()

        if response == 1:
            return -1

        if response == 2:
            return 0

        if response == 3:
            return 1

 
    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()
            if not password:
                return None
        
        passwdDlg.destroy()
        
        return password

    def OnNameTextChanged( self, event ) :
        self.ResetPasswordExpire()
        self.recordChanged = 1
        self.ActivateButtons()

    def OnAccountTextChanged( self, event ) :
        self.ResetPasswordExpire()
        self.recordChanged = 1
        self.ActivateButtons()

    def OnPasswordTextChanged( self, event ) :
        self.ResetPasswordExpire()
        self.recordChanged = 1
        self.ActivateButtons()

    def OnNoteTextChanged( self, one, two ) :
        self.ResetPasswordExpire()
        self.recordChanged = 1
        self.ActivateButtons()

    def OnMainCancelBtn( self, event ) :
        self.ResetPasswordExpire()
        if not self.CheckPassword() :
            return
            
        record = self.DecodeRecord( self.records[self.curRecord][1]
                                    , self.pwDigest )

        self.SetFields( self.records[self.curRecord][0], 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()
        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 )

    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("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("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, account, password, note ) :
        if name != None :
            fld = self.wTree.get_widget("name_text")
            fld.set_text( name )
        if account != None :
            fld = self.wTree.get_widget("account_text")
            fld.set_text( account )
        if password != None :
            fld = self.wTree.get_widget("password_text")
            fld.set_text( password )
        if note != None :
            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()
            buf.insert( iter, note )
            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_OK :
                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.UpdateRecordFromRow()
                self.SetListItem( self.curRecord, self.records[self.curRecord][0] ) 

    def SetListItem( self, row, text ) :
        iter = self.nameList.get_iter( (row,) ) 
        self.nameList.set( iter, 0, text )

    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()
        cur = self.nameView.get_cursor()
        if cur == None or cur[0] == None :
            return

        self.DisplayRecord( cur[0][0] )

    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_OK :
                    self.UpdateRecordFromRow()
                    self.SetListItem( self.curRecord, self.records[self.curRecord][0] ) 

        if not self.CheckPassword() :
            return
            
        record = self.DecodeRecord( self.records[recordNum][1], 
                                    self.pwDigest )
        self.SetFields( self.records[recordNum][0], 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 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 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 :
            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] :
                    print "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 )
        
        for record in self.records :
            self.nameList.append( [ record[0], '' ] )

    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_OK_CANCEL )
	ret = msg_dlg.run()
	msg_dlg.destroy()
        return ret

gtk.gdk.threads_init()

start = Main()
#gobject.timeout_add( start, 10, start.tick )

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