1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# FiSH/Mircryption clone for XChat/HexChat in 100% Python
5#
6# Requirements: PyCrypto, and Python 2 (>=2.5)
7#
8# Copyright 2011 Nam T. Nguyen ( http://www.vithon.org/forum/Thread/show/54 )
9# Released under the BSD license
10#
11# rewritten by trubo/segfault for irc.prooops.eu #py-fishcrypt trubo00@gmail.com
12#
13# fixes by fladd <fladd@fladd.de>
14#
15# irccrypt module is copyright 2009 Bjorn Edstrom ( http://www.bjrn.se/ircsrp )
16# with modification from Nam T. Nguyen and trubo
17#
18# Changelog:
19#   * 5.31
20#      + Minor bugfix when sending messages
21#
22#   * 5.30
23#      + Decrypt own messages (for https://github.com/TingPing/plugins/blob/master/HexChat/mymsg.py)
24#
25#   * 5.20
26#      + Added authentification hash for DH key exchange
27#
28#   * 5.10
29#      + Plugin will now load correctly on Windows
30#      + Fixed bug that crashed the plugin in some cases
31
32#   * 5.00
33#      + Fixed compatibility for networks with identify-msg (e.g. freenode) in key exchange (fladd)
34#      + Added FiSHLiM unload (fladd)
35#      + Changed /ME+ to be compatibile with FiSHLiM (fladd)
36#
37#   * 4.21
38#      + Fixed Empty Action /me
39
40#   * 4.20
41#      + Added support for Stealth mode >> no KeyExchange possible [/SET FISHSTEALTH True/False]
42
43#   * 4.19
44#      + Added support for mIRC CBC KeyExchange, https://github.com/flakes/mirc_fish_10/
45
46#   * 4.18
47#      + Buffix Topic use key from channel not context
48
49#   * 4.17
50#      + CBC Default
51
52#   * 4.16
53#      + Bugfix Topic
54#      + config plaintextmarker in keyprotection
55#      + config parameter DEFAULTPROTECT and DEFAULTCBC
56
57#   * 4.15
58#      + Destroy object
59
60#   * 4.14
61#      + Stable
62
63#   * 4.13
64#      + new NickTrace
65#      + wildcard /KEY search
66#      + msg send to other target are marked with "Message Send"
67#      + Tab Completion for udpate command
68#      + using strxor from the pyCrypto packages if available
69#      + some performance enhancements
70#      + Pseudo Threading for Windows
71
72#   * 4.12
73#      + Beta Support
74
75#   * 4.11
76#      + BugFix /UPDATE
77
78#   * 4.10
79#      + BugFix /FISHSETUP
80
81#   * 4.09
82#      + BugFix again /FISHSETUP /UPDATE
83
84#   * 4.08
85#      + BugFix settings are not saved
86
87#   * 4.07
88#      + new Update function
89
90#   * 4.06
91#      + Small BugFixes
92
93#   * 4.05
94#      + BugFix Windows has no full xchatdir now using scriptpath for fish3.pickle
95
96#   * 4.04
97#      + BugFix notices
98
99#   * 4.03
100#      + BugFix /FISHSETUP
101
102#   * 4.02
103#      + noproxy oprions for /FISHSETUP
104
105#   * 4.01
106#      + BugFix pyBlowfish
107
108#   * 4.00
109#      + Windows Support with pyBlowfish.py and irccrypt now included
110
111#   * 3.31
112#      + BugFix unpack large messages
113
114#   * 3.30
115#      + Added chksum for irccrypt with __module_name__ tags http://pastebin.com/vTrWyBKv
116
117#   * 3.29
118#      + BugFix Update and Threaded Update
119
120#   * 3.28
121#      + /SET [fishcrypt]
122
123#   * 3.27
124#      + BugFix /ME+ in Query
125
126#   * 3.26
127#      + Updates over Proxy
128
129#   * 3.25
130#      + crypted /ME+
131
132#   * 3.24
133#      + BugFix topic 332
134
135#   * 3.23
136#      + BugFix notice send
137
138#   * 3.22
139#      + BugFix
140
141#   * 3.21
142#      + BugFix
143
144#   * 3.20
145#      + partly show incomplete messages
146
147#   * 3.19
148#      + /FISHUPDATE update switch
149
150#   * 3.18
151#      + AUTO CBC Mode only in querys
152
153#   * 3.17
154#      + Highlight Bugfix
155
156#   * 3.16
157#      + Highlight
158
159#   * 3.15
160#      + Bugfixes
161
162#   * 3.13
163#      + split lines if longer then 334 Chars
164#
165#   * 3.12
166#      + add PROTECTKEY to block dh1080 keyexchange on known Keys ( thx ^V^ )
167#
168#   * 3.11
169#      + add Keystorage encryption
170#to
171#   * 3.10
172#      + Fix Path for Windows and provide download URL for pycrypto
173#
174#   * 3.09
175#      + Bugfixes
176#
177#   * 3.08:
178#      + some docu added
179#
180#   * 3.07:
181#      + fixed notice in channel not send to user
182#
183#   * 3.06:
184#	   + support for /msg /msg+ /notice /notice+ (trubo)
185#
186#   * 3.04:
187#	   + new lock design (by target) (trubo)
188#
189#   * 3.01:
190#	   + change switches to be compatible with fish.secure.la/xchat/FiSH-XChat.txt (trubo)
191#
192#	* 3.0:
193#	   + rewritten to class XChatCrypt (trubo)
194#
195#   * 2.0:
196#      + Suport network mask in /key command
197#      + Alias key_exchange to keyx
198#      + Support plaintext marker '+p '
199#      + Support encrypted key store
200#
201#   * 1.0:
202#      + Initial release
203#
204###
205
206
207__module_name__ = 'fishcrypt'
208__module_version__ = '5.31'
209__module_description__ = 'fish encryption in pure python'
210
211ISBETA = ""
212
213UPDATEURL = 'https://raw.githubusercontent.com/fladd/py-fishcrypt/master/fishcrypt.py'
214BETAUPDATEURL = 'https://raw.githubusercontent.com/fladd/py-fishcrypt/master/fishcrypt.py'
215PYBLOWFISHURL = "https://raw.githubusercontent.com/fladd/py-fishcrypt/master/pyBlowfish.py"
216SOCKSIPYURL = 'http://socksipy-branch.googlecode.com/svn/trunk/socks.py'
217
218ONMODES = ["Y","y","j","J","1","yes","on","ON","Yes","True","true"]
219YESNO = lambda x: (x==0 and "N") or "Y"
220
221import sys
222import os
223import re
224import base64
225import hashlib
226import struct
227import time
228
229from math import log
230
231try:
232    import xchat
233except ImportError:
234    sys.exit("should be run from xchat plugin with python enabled")
235
236try:
237    import cPickle as pickle
238except ImportError:
239    import pickle
240
241## check for Windows
242import platform
243sep = "/"
244isWindows = (platform.system() == "Windows")
245if isWindows:
246    sep = "\\"
247
248## append current path
249if isWindows:
250    # Try XChat
251    scriptname = os.getenv('APPDATA')+"\\XChat\\addons\\fishcrypt.py"
252    if not os.path.exists(scriptname):
253        # Try HexChat
254        scriptname = os.getenv('APPDATA')+"\\HexChat\\addons\\fishcrypt.py"
255        if not os.path.exists(scriptname):
256            # Try portable install
257            scriptname = sys.path[0]+"\\addons\\fishcrypt.py"
258else:
259    import inspect
260    scriptname = inspect.currentframe().f_code.co_filename
261script = "".join(scriptname.split(sep)[-1:])
262path = sep.join(scriptname.split(sep)[:-1])
263sys.path.insert(1,path)
264SCRIPTCHKSUM = hashlib.sha1(open(scriptname,'rb').read()).hexdigest()
265REQUIRESETUP = False
266
267try:
268    import Crypto.Cipher.Blowfish as cBlowfish
269except ImportError:
270    try:
271        import pyBlowfish as cBlowfish
272        pyBlowfishlocation = "%s.py" % str(cBlowfish)[str(cBlowfish).find("from '")+6:str(cBlowfish).find(".py")]
273        chksum = hashlib.sha1(open(pyBlowfishlocation,'rb').read()).hexdigest()
274        validVersion = {'35c1b6cd5af14add86dc0cf3f0309a185c308dcd':0.4,'877ae9de309685c975a6d120760c1ff9b4c55719':0.5, '57117e7c9c7649bf490589b7ae06a140e82664c6':0.5}.get(chksum,-1)
275        if validVersion == -1:
276            print "\0034** Loaded pyBlowfish.py with checksum: %s is untrusted" % (chksum)
277        else:
278            if validVersion < 0.5:
279                print "\0034** Loaded pyBlowfish.py (%.1f) with checksum: %s is too old" % (validVersion,chksum)
280                REQUIRESETUP = True
281            else:
282                print "\0033** Loaded pyBlowfish.py Version %.1f with checksum: %s" % (validVersion,chksum)
283
284    except ImportError:
285        import platform
286        print "\002\0034No Blowfish implementation"
287        if not isWindows:
288            print "This module requires PyCrypto / The Python Cryptographic Toolkit."
289            print "Get it from http://www.dlitz.net/software/pycrypto/. or"
290        else:
291            path = path.replace(sep,sep*2)
292        print "Download Python only Blowfish at %s" % PYBLOWFISHURL
293        print "or type \002/FISHSETUP\002 for automatic install of that"
294
295        REQUIRESETUP = True
296
297try:
298    xchat.command("UNLOAD FiSHLiM")
299except:
300    pass
301
302try:
303    from Crypto.Util.strxor import strxor as xorstring
304except ImportError:
305    ## use slower python only xor
306    def xorstring(a, b): # Slow.
307        """xor string a and b, both of length blocksize."""
308        xored = []
309        for i in xrange(8):
310            xored.append( chr(ord(a[i]) ^ ord(b[i])) )
311        return "".join(xored)
312
313if not isWindows:
314    from threading import Thread
315else:
316    class Thread:
317        def __init__(self,target=None,args=[],kwargs={},name='Thread*'):
318            self.__target = target
319            self.__args = args
320            self.__kwargs = kwargs
321            self.__name = name
322            self.__hook = None
323        def start(self):
324            print "-Starting Pseudo Thread"
325            self.__hook = xchat.hook_timer(1,self.__thread,(self.__target,self.__args,self.__kwargs))
326        def __thread(self,userdata):
327            try:
328                _thread,args,kwargs = userdata
329                _thread(*args,**kwargs)
330            finally:
331                xchat.unhook(self.__hook)
332                self.__hook = None
333                return False
334
335import socket
336REALSOCKET = socket.socket
337
338def makedict(**kwargs):
339    return kwargs
340
341COLOR = makedict(white="\0030", black="\0031", blue="\0032", red="\0034",
342    dred="\0035", purple="\0036", dyellow="\0037", yellow="\0038", bgreen="\0039",
343    dgreen="\00310", green="\00311", bpurple="\00313", dgrey="\00314",
344    lgrey="\00315", close="\003")
345
346
347class SecretKey(object):
348    def __init__(self, dh, key=None,protectmode=False,cbcmode=False):
349        self.dh = dh
350        self.key = key
351        self.cbc_mode = cbcmode
352        self.protect_mode = protectmode
353        self.active = True
354        self.cipher = 0
355        self.keyname = (None,None)
356    def __str__(self):
357        return "%s@%s" % self.keyname
358
359def proxyload(_thread,_useproxy,doExtra):
360    socket.socket = REALSOCKET
361    if xchat.get_prefs('net_proxy_type') > 0 and _useproxy:
362        try:
363            import socks
364        except ImportError:
365            print "\0034python-socksipy not installed"
366            print "sudo apt-get install python-socksipy"
367            print "or install %s" % SOCKSIPYURL
368            print "or just use the noproxy option with /FISHUPDATE and /FISHSETUP"
369            return xchat.EAT_ALL
370
371        proxytype = [0,-1,socks.PROXY_TYPE_SOCKS4,socks.PROXY_TYPE_SOCKS5,socks.PROXY_TYPE_HTTP,-1][xchat.get_prefs('net_proxy_type')]
372        nameproxytype = ['','Socks4a','Socks5','HTTP','']
373        if proxytype < 0:
374            print "\0034Proxytype not suported for updates"
375            return xchat.EAT_ALL
376        proxyuser = xchat.get_prefs('net_proxy_user')
377        proxypass = xchat.get_prefs('net_proxy_pass')
378        if len(proxyuser) < 1 or len(proxypass) < 1:
379            proyxuser = proxypass = None
380        socks.setdefaultproxy(proxytype,xchat.get_prefs('net_proxy_host'),xchat.get_prefs('net_proxy_port'),rdns=True,username=proxyuser,password=proxypass)
381        print "\00310using xchat proxy settings \0037Type: %s Host: %s Port: %s" % (nameproxytype[proxytype],xchat.get_prefs('net_proxy_host'),xchat.get_prefs('net_proxy_port'))
382
383        ## Replace default socket
384        socket.socket = socks.socksocket
385
386    import urllib2
387    _thread(urllib2,doExtra)
388
389def destroyObject(userdata):
390    global loadObj
391    del loadObj
392    return False
393
394class XChatCrypt:
395    def __init__(self):
396        print "%sFishcrypt Version %s %s\003" % (COLOR['blue'],__module_version__,ISBETA)
397        print "SHA1 checksum: %r" % SCRIPTCHKSUM
398        self.active = True
399        self.__KeyMap = {}
400        self.__TargetMap = {}
401        self.__lockMAP = {}
402        self.config = {
403            'PLAINTEXTMARKER' : '+p',
404            'DEFAULTCBC' : True,
405            'DEFAULTPROTECT' : False,
406            'FISHUPDATETIMEOUT' : 30,
407            'MAXMESSAGELENGTH' : 300,
408            'USEPROXYUPDATE' : True,
409            'FISHBETAVERSION': True,
410            'FISHDEVELOPDEBUG': False,
411            'AUTOBACKUP': True,
412            'FISHSTEALTH': False,
413        }
414        self.status = {
415            'CHKPW': None,
416            'DBPASSWD' : None,
417            'CRYPTDB' : False,
418            'LOADED' : True
419        }
420        self.__update_thread = None
421        self._updatedSource = None
422        self.__hooks = []
423        self.__hooks.append(xchat.hook_command('SETKEY', self.set_key, help='set a new key for a nick or channel /SETKEY <nick>/#chan [new_key]'))
424        self.__hooks.append(xchat.hook_command('KEYX', self.key_exchange, help='exchange a new pub key, /KEYX <nick>'))
425        self.__hooks.append(xchat.hook_command('KEY', self.show_key, help='list key of a nick or channel or all (*), /KEY [nick/#chan/*]' ))
426        self.__hooks.append(xchat.hook_command('DELKEY', self.del_key, help='remove key, /DELKEY <nick>/#chan/*'))
427        self.__hooks.append(xchat.hook_command('CBCMODE', self.set_cbc, help='set or shows cbc mode for (current) channel/nick , /CBCMODE [<nick>] <0|1>'))
428        self.__hooks.append(xchat.hook_command('PROTECTKEY', self.set_protect, help='sets or shows key protection mode for (current) nick, /PROTECTKEY [<nick>] <0|1>'))
429        self.__hooks.append(xchat.hook_command('ENCRYPT', self.set_act, help='set or shows encryption on for (current) channel/nick , /ENCRYPT [<nick>] <0|1>'))
430
431        self.__hooks.append(xchat.hook_command('PRNCRYPT', self.prn_crypt, help='print msg encrpyted localy , /PRNCRYPT <msg>'))
432        self.__hooks.append(xchat.hook_command('PRNDECRYPT', self.prn_decrypt, help='print msg decrpyted localy , /PRNDECRYPT <msg>'))
433
434        self.__hooks.append(xchat.hook_command('UPDATE', self.update, help='Update this Script'))
435        self.__hooks.append(xchat.hook_command('FISHUPDATE', self.fishupdate, help='Update this Script'))
436
437        ## check for password sets
438        self.__hooks.append(xchat.hook_command('SET',self.settings))
439        self.__hooks.append(xchat.hook_command('DBPASS',self.set_dbpass))
440        self.__hooks.append(xchat.hook_command('DBLOAD',self.set_dbload))
441
442        self.__hooks.append(xchat.hook_command('HELP',self.get_help))
443
444        self.__hooks.append(xchat.hook_command('', self.outMessage))
445        self.__hooks.append(xchat.hook_command('ME+', self.outMessageCmd))
446        self.__hooks.append(xchat.hook_command('MSG', self.outMessageCmd))
447        self.__hooks.append(xchat.hook_command('MSG+', self.outMessageForce))
448        self.__hooks.append(xchat.hook_command('NOTICE', self.outMessageCmd))
449        self.__hooks.append(xchat.hook_command('NOTICE+', self.outMessageForce))
450
451        self.__hooks.append(xchat.hook_server('notice', self.on_notice,priority=xchat.PRI_HIGHEST))
452        self.__hooks.append(xchat.hook_server('332', self.server_332_topic,priority=xchat.PRI_HIGHEST))
453
454        self.__hooks.append(xchat.hook_print('Key Press',self.tabComplete))
455
456        self.__hooks.append(xchat.hook_print('Notice Send',self.on_notice_send, 'Notice',priority=xchat.PRI_HIGHEST))
457        self.__hooks.append(xchat.hook_print('Change Nick', self.nick_trace))
458        self.__hooks.append(xchat.hook_print('Channel Action', self.inMessage, 'Channel Action',priority=xchat.PRI_HIGHEST))
459        self.__hooks.append(xchat.hook_print('Private Action to Dialog', self.inMessage, 'Private Action to Dialog',priority=xchat.PRI_HIGHEST))
460        self.__hooks.append(xchat.hook_print('Private Action ', self.inMessage, 'Private Action',priority=xchat.PRI_HIGHEST))
461        self.__hooks.append(xchat.hook_print('Channel Message', self.inMessage, 'Channel Message',priority=xchat.PRI_HIGHEST))
462        self.__hooks.append(xchat.hook_print('Private Message to Dialog', self.inMessage, 'Private Message to Dialog',priority=xchat.PRI_HIGHEST))
463        self.__hooks.append(xchat.hook_print('Private Message', self.inMessage, 'Private Message',priority=xchat.PRI_HIGHEST))
464        self.__hooks.append(xchat.hook_print('Your Message', self.inMessage, 'Your Message',priority=xchat.PRI_HIGHEST))
465        self.__hooks.append(xchat.hook_unload(self.__destroy))
466        self.loadDB()
467
468    def __destroy(self,userdata):
469        for hook in self.__hooks:
470            xchat.unhook(hook)
471        destroyObject(None)
472
473    def __del__(self):
474        print "\00311fishcrypt.py successful unloaded"
475
476    def get_help(self,word, word_eol, userdata):
477        if len(word) < 2:
478            print "\n\0033 For fishcrypt.py help type /HELP FISHCRYPT"
479            return xchat.EAT_NONE
480        if word[1].upper() == "FISHCRYPT":
481            print ""
482            print "\002\0032 ****  fishcrypt.py Version: %s %s ****" % (__module_version__,ISBETA)
483            if self.config['FISHBETAVERSION']:
484                print "\0036Beta download %s" % (BETAUPDATEURL)
485
486            print "\0036 %s" % UPDATEURL
487            print "\n"
488            print " \002\00314***************** Fishcrypt Help ********************"
489            print " -----------------------------------------------------"
490            print "/MSG+ \00314send crypted msg regardless of /ENCRYPT setting"
491            print "/NOTICE+ \00314send crypted notice regardless of /ENCRYPT setting"
492            print "/ME+ \00314send crypted CTCP ACTION"
493            print "/SETKEY \00314set a new key for a nick or channel"
494            print "/KEYX \00314exchange pubkey for dialog"
495            print "/KEY \00314show Keys"
496            print "/DELKEY \00314delete Keys"
497            print "/CBCMODE \00314enable/disable CBC Mode for this Key"
498            print "/ENCRYPT \00314enable/disable encryption for this Key"
499            print "/PROTECTKEY \00314enable/disable protection for keyx key exchange"
500            print "/DBPASS \00314set/change the passphrase for the Key Storage"
501            print "/DBLOAD \00314loads the Key Storage"
502            print "/PRNDECRYPT \00314decrypts messages localy"
503            print "/PRNCRYPT \00314encrypts messages localy"
504            print "/FISHUPDATE \00314check online for new Version and update"
505            print "/SET [fishcrypt] \00314show/set fishcrypt settings"
506            return xchat.EAT_ALL
507
508    def tabComplete(self,word, word_eol, userdata):
509        if word[0] not in ["65289","65056"]:
510            return xchat.EAT_NONE
511        input = xchat.get_info('inputbox')
512        if input.upper().startswith("/UPDATE FISHCRYPT I"):
513            newinput = "/UPDATE FISHCRYPT INSTALL"
514        elif input.upper().startswith("/UPDATE FISHCRYPT D"):
515            newinput = "/UPDATE FISHCRYPT DIFF"
516        elif input.upper().startswith("/UPDATE FISHCRYPT C"):
517            newinput = "/UPDATE FISHCRYPT CHANGES"
518        elif input.upper().startswith("/UPDATE FISHCRYPT L"):
519            newinput = "/UPDATE FISHCRYPT LOAD"
520        elif input.upper() == "/UPDATE FISHCRYPT ":
521            print "LOAD INSTALL DIFF CHANGES"
522            return xchat.EAT_NONE
523        elif input.upper().startswith("/UPDATE F"):
524            newinput = "/UPDATE FISHCRYPT "
525        elif input.upper().startswith("/HELP F"):
526            newinput = "/HELP FISHCRYPT "
527        elif input.upper().startswith("/SET F"):
528            newinput = "/SET FISHCRYPT "
529        else:
530            return xchat.EAT_NONE
531        xchat.command("SETTEXT %s" % newinput)
532        xchat.command("SETCURSOR %d" % len(newinput))
533        return xchat.EAT_PLUGIN
534
535
536    def fishupdate(self,word, word_eol, userdata):
537        return self.update(["UPDATE","FISHCRYPT","INSTALL"],None,None)
538
539    def update(self,word, word_eol, userdata):
540        useproxy = self.config['USEPROXYUPDATE']
541        if len(word) <3:
542            print "\00313Fishcrypt.py Updater"
543            print "\00313/UPDATE FISHCRYPT [LOAD,CHANGES,DIFF,INSTALL]"
544            return xchat.EAT_XCHAT
545        if word[1].upper() != "FISHCRYPT":
546            return xchat.EAT_NONE
547        if self.__update_thread:
548            print "\0034Update Thread already running"
549            return xchat.EAT_ALL
550        _doExtra = None
551        if word[2].lower() == "diff":
552            if self._updatedSource:
553                self._updateDiff(xchat.get_context())
554            else:
555                _doExtra = self._updateDiff
556        if word[2].lower() == "changes":
557            if self._updatedSource:
558                self._updateChanges(xchat.get_context())
559            else:
560                _doExtra = self._updateChanges
561        if word[2].lower() == "install":
562            if self._updatedSource:
563                self._updateInstall(xchat.get_context())
564            else:
565                _doExtra = self._updateInstall
566        if word[2].lower() == "load" or _doExtra:
567            proxyload(self._update,useproxy,_doExtra)
568
569        return xchat.EAT_ALL
570
571    def _update(self,urllib2,doExtra):
572        self.__update_thread = Thread(target=self.__update,kwargs={'urllib2':urllib2,'context':xchat.get_context(),'doExtra':doExtra},name='fishcrypt_update')
573        self.__update_thread.start()
574
575    def _updateInstall(self,context):
576        try:
577            try:
578                __fd = open(scriptname,"wb")
579                __fd.write(self._updatedSource)
580            finally:
581                __fd.close()
582            context.prnt( "\00310UPDATE Complete \r\nplease reload the script (/py reload %s)" % (script,) )
583        except:
584            context.prnt( "\002\0034UPDATE FAILED" )
585            raise
586
587    def _updateDiff(self,context):
588        currentscript = open(scriptname,"rb").read()
589        import difflib
590        for line in difflib.unified_diff(currentscript.splitlines(1),self._updatedSource.splitlines(1)):
591            context.prnt( line)
592
593    def _updateChanges(self,context):
594        currentscript = open(scriptname,"rb").read()
595        import difflib
596        for line in difflib.ndiff(currentscript[currentscript.find("# Changelog:"):currentscript.find("__module_name__")].splitlines(1),self._updatedSource[self._updatedSource.find("# Changelog:"):self._updatedSource.find("__module_name__")].splitlines(1)):
597            if len(line) > 2:
598                if line[0] in ["+","-"]:
599                    context.prnt( line[2:])
600
601    def __update(self,urllib2,context,doExtra):
602        url = UPDATEURL
603        if self.config['FISHBETAVERSION']:
604            url = BETAUPDATEURL
605        context.prnt("\0038.....checking for updates at %r... please wait ...." % url)
606        try:
607            try:
608                __updatescript = urllib2.urlopen(url,timeout=self.config['FISHUPDATETIMEOUT']).read()
609                __updateversion = re.search("__module_version__ = '([0-9]+\.[0-9]+)'",__updatescript)
610                if __updateversion:
611                    if float(__module_version__) < float(__updateversion.group(1)) or ISBETA != "":
612                        updatechksum = hashlib.sha1(__updatescript).hexdigest()
613                        if SCRIPTCHKSUM <> updatechksum:
614                            self._updatedSource = __updatescript
615                            context.prnt( "\00310Download Version %s with checksum %r complete" % (__updateversion.group(1),updatechksum))
616                        else:
617                            context.prnt( "\00310No new version available - checksums match")
618                    else:
619                        context.prnt( "\0032%sVersion %s is up to date (found Version %s)" % (__module_name__,__module_version__,__updateversion.group(1)) )
620                else:
621                    context.prnt( "\0034NO VALID PLUGIN FOUND AT %s" % (url,) )
622            except urllib2.URLError,err:
623                context.prnt( "\002\0034LOAD FAILED" )
624                context.prnt( "%r" % (err,) )
625
626            except:
627                context.prnt( "\002\0034LOAD FAILED" )
628                context.prnt("%r" % (sys.exc_info(),))
629        finally:
630            self.__update_thread = None
631            context.prnt( "\00310Update Thread finished" )
632        if doExtra and self._updatedSource:
633            doExtra(context)
634
635
636    ## Load key storage
637    def loadDB(self):
638        data = db = None
639        try:
640            try:
641                hnd = open(os.path.join(path,'fish3.pickle'),'rb')
642                data = hnd.read()
643                ## set DB loaded to False as we have a file we don't want to create a new
644                self.status['LOADED'] = False
645            except:
646                return
647        finally:
648            try:
649                hnd.close()
650            except:
651                pass
652        if data:
653            try:
654                db = pickle.loads(data)
655                print "%sUnencrypted Key Storage loaded" % (COLOR['bpurple'],)
656            except pickle.UnpicklingError:
657                ## ignore if file is invalid
658                if data.startswith("+OK *"):
659                    self.status['CRYPTDB'] = True
660                    if self.status['DBPASSWD']:
661                        try:
662                            algo = BlowfishCBC(self.status['DBPASSWD'])
663                            decrypted = mircryption_cbc_unpack(data,algo)
664                            db = pickle.loads(decrypted)
665                            print "%sEncrypted Key Storage loaded" % (COLOR['green'],)
666                        except pickle.UnpicklingError:
667                            self.status['DBPASSWD'] = None
668                            print "%sKey Storage can't be loaded with this password" % (COLOR['dred'],)
669                            print "use /DBLOAD to load it later"
670                    else:
671                        xchat.command('GETSTR ""  "SET fishcrypt_passload" "Enter your Key Storage Password"')
672                pass
673        if type(db) == dict:
674            self.status['LOADED'] = True
675            ## save temp keymap
676            oldKeyMap = self.__KeyMap
677            oldTargetMap = self.__TargetMap
678            ## fill dict with the loaded Keymap
679            self.__KeyMap = db.get("KeyMap",{})
680            self.__TargetMap = db.get("TargetMap",{})
681            self.__KeyMap.update(oldKeyMap)
682            self.__TargetMap.update(oldTargetMap)
683            for key in self.__KeyMap.keys():
684                self.__KeyMap[key].keyname = key
685                if not hasattr(self.__KeyMap[key],'protect_mode'):
686                    self.__KeyMap[key].protect_mode = False
687            self.cleanUpTargetMap()
688
689            ## only import valid config values
690            for key in self.config.keys():
691                try:
692                    self.config[key] = db["Config"][key]
693                except KeyError:
694                    pass
695            if self.config['FISHDEVELOPDEBUG']:
696                self.__hooks.append(xchat.hook_command('FISHEVAL',self.__evaldebug))
697
698    def cleanUpTargetMap(self):
699        ## DB Cleanup
700        for network in self.__TargetMap.values():
701            for target,value in network.items():
702                if type(value[1]) <> SecretKey or value[0] < time.time() - 60*60*24*7 or value[1] not in self.__KeyMap.values():
703                    del network[target]
704                    print "Expired: %r %r" % (target,value)
705
706
707    ## save keys to storage
708    def saveDB(self):
709        self.cleanUpTargetMap()
710        if not self.status['LOADED']:
711            print "Key Storage not loaded, no save. use /DBLOAD to load it"
712            return
713        try:
714            data = pickle.dumps({
715                'KeyMap': self.__KeyMap,
716                'TargetMap': self.__TargetMap,
717                'Config': self.config,
718                'Version': __module_version__
719            })
720            hnd = open(os.path.join(path,'fish3.pickle'),'wb')
721            if self.status['DBPASSWD']:
722                algo = BlowfishCBC(self.status['DBPASSWD'])
723                encrypted = mircryption_cbc_pack(data,algo)
724                data = encrypted
725                self.status['CRYPTDB'] = True
726            else:
727                self.status['CRYPTDB'] = False
728            hnd.write(data)
729        finally:
730            hnd.close()
731
732    def __evaldebug(self,word, word_eol, userdata):
733        eval(compile(word_eol[1],'develeval','exec'))
734        return xchat.EAT_ALL
735
736    def set_dbload(self,word, word_eol, userdata):
737        self.loadDB()
738        return xchat.EAT_ALL
739
740    def set_dbpass(self,word, word_eol, userdata):
741        xchat.command('GETSTR "" "SET fishcrypt_passpre" "New Password"')
742        return xchat.EAT_ALL
743
744    ## set keydb passwd
745    def settings(self,word, word_eol, userdata):
746        fishonly = False
747        if len(word) == 2:
748            if word[1].upper() == "FISHCRYPT":
749                fishonly = True
750        if len(word) < 2 or fishonly:
751            ## not for us
752            #print "fishcrypt_pass%s%s%s: \003%r" % (COLOR['blue'],"."*16,COLOR['green'],self.status['DBPASSWD'])
753            for key in self.config:
754                keyname = "%s%s" % (key,"."*20)
755                print "\00312%.29s: %s" % (keyname,str(self.config[key]))
756            if fishonly:
757                return xchat.EAT_ALL
758            return xchat.EAT_NONE
759
760
761        if word[1] == "fishcrypt_passpre":
762            if len(word) == 2:
763                self.status['CHKPW'] = ""
764            else:
765                self.status['CHKPW'] = word_eol[2]
766            xchat.command('GETSTR ""  "SET fishcrypt_pass" "Repeat the Password"')
767            return xchat.EAT_ALL
768
769        if word[1] == "fishcrypt_pass":
770            if len(word) == 2:
771                if self.status['CHKPW'] <> "" and self.status['CHKPW'] <> None:
772                    print "Passwords don't match"
773                    self.status['CHKPW'] = None
774                    return xchat.EAT_ALL
775                self.status['DBPASSWD'] = None
776                print "%sPassword removed and Key Storage decrypted" % (COLOR['dred'],)
777                print "%sWarning Keys are plaintext" % (COLOR['dred'],)
778            else:
779                if self.status['CHKPW'] <> None and self.status['CHKPW'] <> word_eol[2]:
780                    print "Passwords don't match"
781                    self.status['CHKPW'] = None
782                    return xchat.EAT_ALL
783                if len(word_eol[2]) < 8 or len(word_eol[2]) > 56:
784                    print "Passwords must be between 8 and 56 chars"
785                    self.status['CHKPW'] = None
786                    return xchat.EAT_ALL
787                self.status['DBPASSWD'] = word_eol[2]
788                ## don't show the pw on console if set per GETSTR
789                if self.status['CHKPW'] == None:
790                    print "%sPassword for Key Storage encryption set to %r" % (COLOR['dred'],self.status['DBPASSWD'])
791                else:
792                    print "%sKey Storage encrypted" % (COLOR['dred'])
793            self.status['CHKPW'] = None
794            self.saveDB()
795            return xchat.EAT_ALL
796
797        if word[1] == "fishcrypt_passload":
798            if len(word) > 2:
799                if len(word_eol[2]) < 8 or len(word_eol[2]) > 56:
800                    print "Password not between 8 and 56 chars"
801                else:
802                    self.status['DBPASSWD'] = word_eol[2]
803                    self.loadDB()
804            else:
805                print "Key Storage Not loaded"
806                self.status['DBPASSWD'] = None
807            return xchat.EAT_ALL
808
809        key = word[1].upper()
810        if key in self.config.keys():
811            if len(word) <3:
812                keyname = "%s%s" % (key,"."*20)
813                print "\00312%.29s: %s" % (keyname,str(self.config[key]))
814            else:
815                try:
816                    if type(self.config[key]) == bool:
817                        self.config[key] = bool(word[2] in ONMODES)
818                    else:
819                        self.config[key] = type(self.config[key])(word_eol[2])
820                    print "\0035Set %r to %r" % (key,word_eol[2])
821                    self.saveDB()
822                except ValueError:
823                    print "\0034Invalid Config Value %r for %s" % (word_eol[2],key)
824            return xchat.EAT_ALL
825
826        return xchat.EAT_NONE
827
828    ## incoming notice received
829    def on_notice(self,word, word_eol, userdata):
830        ## check if this is not allready processed
831        if self.__chk_proc():
832            return xchat.EAT_NONE
833
834        ## check if DH Key Exchange
835        if word[3].startswith(":") and word[3].endswith('DH1080_FINISH'):
836            return self.dh1080_finish(word, word_eol, userdata)
837        elif word[3].startswith(":") and word[3].endswith('DH1080_INIT'):
838            return self.dh1080_init(word, word_eol, userdata)
839
840        ## check for encrypted Notice
841        elif word[3].startswith(':') and (word[3].endswith(':+OK') or word[3].startswith(':mcps')):
842
843            ## rewrite data to pass to default inMessage function
844            ## change full ident to nick only
845            nick = self.get_nick(word[0])
846            target = word[2]
847            speaker = nick
848            ## strip :: from message
849            if word[3].endswith("+OK"):
850                idx = len(word[3]) - len("+OK")
851            elif word[3].endswith("mcps"):
852                idx = len(word[3]) - len("mcps")
853            message = word_eol[3][idx:]
854            if target.startswith("#"):
855                id = self.get_id()
856                speaker = "## %s" % speaker
857            else:
858                id = self.get_id(nick=nick)
859            #print "DEBUG(crypt): key: %r word: %r" % (id,word,)
860            key = self.find_key(id)
861            ## if no key found exit
862            if not key:
863                return xchat.EAT_NONE
864
865            ## decrypt the message
866            try:
867                sndmessage = self.decrypt(key,message)
868            except:
869                sndmessage = None
870            isCBC=0
871            if message.startswith("+OK *"):
872                isCBC=1
873            failcol = ""
874
875            ## if decryption was possible check for invalid chars
876            if sndmessage:
877                try:
878                    message = sndmessage.decode("UTF8").encode("UTF8")
879                    ## mark nick for encrypted msgg
880                    speaker = "%s %s" % ("°"*(1+isCBC),speaker)
881                except UnicodeError:
882                    try:
883                        message = unicode(sndmessage,encoding='iso8859-1',errors='ignore').encode('UTF8')
884                        ## mark nick for encrypted msgg
885                        speaker = "%s %s" % ("°"*(1+isCBC),speaker)
886                    except:
887                        raise
888                    ## send the message to local xchat
889                    #self.emit_print(userdata,speaker,message)
890                    #return xchat.EAT_XCHAT
891                except:
892                    ## mark nick with a question mark
893                    speaker = "?%s" % (speaker)
894                    failcol = "\003"
895            else:
896                failcol = "\003"
897            ## mark the message with \003, it failed to be processed and there for the \003+OK  will no longer be excepted as encrypted so it wont loop
898            self.emit_print(userdata,speaker,"%s%s" % (failcol,message))
899            return xchat.EAT_XCHAT
900#            return self.inMessage([nick,msg], ["%s %s" % (nick,msg),msg], userdata)
901
902        ## ignore everything else
903        else:
904            #print "DEBUG: %r %r %r" % (word, word_eol, userdata)
905            return xchat.EAT_NONE
906
907    ## local notice send messages
908    def on_notice_send(self,word, word_eol, userdata):
909        ## get current nick
910        target = xchat.get_context().get_info('nick')
911        #print "DEBUG_notice_send: %r - %r - %r %r" % (word,word_eol,userdata,nick)
912
913        ## check if this is not allready processed
914        if self.__chk_proc(target=target):
915            return xchat.EAT_NONE
916
917        ## get the speakers nick only from full ident
918        speaker = self.get_nick(word[0])
919
920        ## strip first : from notice
921        message = word_eol[1][1:]
922        if message.startswith('+OK ') or message.startswith('mcps '):
923            ## get the key id from the speaker
924            id = self.get_id(nick=speaker)
925            key = self.find_key(id)
926
927            ## if no key available for the speaker exit
928            if not key:
929                return xchat.EAT_NONE
930
931            ## decrypt the message
932            sndmessage = self.decrypt(key,message)
933            isCBC = 0
934            if message.startswith("+OK *"):
935                isCBC = 1
936                if not target.startswith("#"):
937                    ## if we receive a messge with CBC enabled we asume the partner can also except it so activate it
938                    key.cbc_mode = True
939
940            ## if decryption was possible check for invalid chars
941
942            if sndmessage:
943                try:
944                    message = sndmessage.decode("UTF8").encode("UTF8")
945                    ## mark nick for encrypted msgg
946                    speaker = "%s %s" % ("°"*(1+isCBC),speaker)
947                except:
948                    ## mark nick with a question mark
949                    speaker = "?%s" % (speaker)
950                    ## send original message because invalid chars
951                    message = message
952
953            ## send the message back to incoming notice but with locked target status so it will not be processed again
954            self.emit_print("Notice Send",speaker,message,target=target)
955            return xchat.EAT_XCHAT
956        return xchat.EAT_NONE
957
958    ## incoming messages
959    def inMessage(self,word, word_eol, userdata):
960        ## if message is allready processed ignore
961        if self.__chk_proc() or len(word_eol) < 2:
962            return xchat.EAT_PLUGIN
963        speaker = word[0]
964        message = word_eol[1]
965        #print "DEBUG(INMsg): %r - %r - %r" % (word,word_eol,userdata)
966        # if there is mode char, remove it from the message
967        if len(word_eol) >= 3:
968            #message = message[ : -(len(word_eol[2]) + 1)]
969            message = message[:-2]
970
971        ## check if message is crypted
972        if message.startswith('+OK ') or message.startswith('mcps '):
973            target = None
974            if userdata == "Private Message":
975                target = speaker
976            id = self.get_id(nick=target)
977            target,network = id
978            key = self.find_key(id)
979
980            ## if no key found exit
981            if not key:
982                return xchat.EAT_NONE
983
984            ## decrypt the message
985            try:
986                sndmessage = self.decrypt(key,message)
987            except:
988                sndmessage = None
989            isCBC=0
990            if message.startswith("+OK *"):
991                isCBC=1
992                if not target.startswith("#"):
993                    ## if we receive a messge with CBC enabled we asume the partner can also except it so activate it
994                    key.cbc_mode = True
995
996            failcol = ""
997
998            ## if decryption was possible check for invalid chars
999            action = False
1000            if sndmessage:
1001                if sndmessage.startswith("\001ACTION"):
1002                    sndmessage = sndmessage.lstrip("\001ACTION")
1003                    action = True
1004                try:
1005                    message = sndmessage.decode("UTF8").encode("UTF8")
1006                    ## mark nick for encrypted msgg
1007                    speaker = "%s %s" % ("°"*(1+isCBC),speaker)
1008                except UnicodeError:
1009                    try:
1010                        message = unicode(sndmessage,encoding='iso8859-1',errors='ignore').encode('UTF8')
1011                        ## mark nick for encrypted msgg
1012                        speaker = "%s %s" % ("°"*(1+isCBC),speaker)
1013                    except:
1014                        raise
1015                    ## send the message to local xchat
1016                    #self.emit_print(userdata,speaker,message)
1017                    #return xchat.EAT_XCHAT
1018                except:
1019                    ## mark nick with a question mark
1020                    speaker = "?%s" % (speaker)
1021                    failcol = "\003"
1022            else:
1023                failcol = "\003"
1024            ## mark the message with \003, it failed to be processed and there for the \003+OK  will no longer be excepted as encrypted so it wont loop
1025            if action:
1026                self.emit_print('Channel Action',  speaker, message.strip())
1027            else:
1028                self.emit_print(userdata,speaker,"%s%s" % (failcol,message))
1029            return xchat.EAT_ALL
1030
1031        return xchat.EAT_NONE
1032
1033    def decrypt(self,key,msg):
1034        ## check for CBC
1035        if 3 <= msg.find(' *') <= 4:
1036            decrypt_clz = BlowfishCBC
1037            decrypt_func = mircryption_cbc_unpack
1038        else:
1039            decrypt_clz = Blowfish
1040            decrypt_func = blowcrypt_unpack
1041        try:
1042            b = decrypt_clz(key.key)
1043            #if msg[-2:-1] == " ":
1044            #	msg = msg[:-2]
1045            ret = decrypt_func(msg, b)
1046        except MalformedError:
1047            try:
1048                cut = (len(msg) -4)%12
1049                if cut > 0:
1050                    msg = msg[:cut *-1]
1051                    ret = "%s%s" % ( decrypt_func(msg, b), " \0038<<incomplete>>" * (cut>0))
1052                else:
1053                    #print "Error Malformed %r" % len(msg)
1054                    ret = None
1055            except MalformedError:
1056                #print "Error2 Malformed %r" % len(msg)
1057                ret = None
1058        except:
1059            print "Decrypt ERROR"
1060            ret = None
1061        return ret
1062
1063
1064    ## mark outgoing message being  prefixed with a command like /notice /msg ...
1065    def outMessageCmd(self,word, word_eol, userdata):
1066        return self.outMessage(word, word_eol, userdata,command=True)
1067
1068    ## mark outgoing message being prefixed with a command that enforces encryption like /notice+ /msg+
1069    def outMessageForce(self,word, word_eol, userdata):
1070        return self.outMessage(word, word_eol, userdata, force=True,command=True)
1071
1072    ## the outgoing messages will be proccesed herre
1073    def outMessage(self,word, word_eol, userdata,force=False,command=False):
1074        ## check if allready processed
1075        if self.__chk_proc():
1076            return xchat.EAT_NONE
1077
1078        ## get the id
1079        id = self.get_id()
1080        target,network = id
1081        ## check if message is prefixed wit a command like /msg /notice
1082        action = False
1083        if command:
1084
1085            if len(word) < (word[0].upper().startswith("ME") and 2 or 3):
1086                print "Usage: %s <nick/channel> <message>, sends a %s.%s are a type of message that should be auto reacted to" % (word[0],word[0],word[0])
1087                return xchat.EAT_ALL
1088            ## notice and notice+
1089            if word[0].upper().startswith("NOTICE"):
1090                command = "NOTICE"
1091            else:
1092                command = "PRIVMSG"
1093            if word[0].upper().startswith("ME"):
1094                action = True
1095                message = word_eol[1]
1096            else:
1097                ## the target is first parameter after the command, not the current channel
1098                target = word[1]
1099                ## change id
1100                id = (target,network)
1101                ## remove command and target from message
1102                message = word_eol[2]
1103        else:
1104            command = "PRIVMSG"
1105            if len(word_eol) < 1:
1106                return xchat.EAT_NONE
1107            else:
1108                message = word_eol[0]
1109        sendmsg = ''
1110        ## try to get a key for the target id
1111        key = self.find_key(id)
1112
1113        ## my own nick
1114        nick = xchat.get_context().get_info('nick')
1115
1116        #print "DEBUG(outMsg1)(%r) %r : %r %r" % (id,xchat.get_context().get_info('network'),word,nick)
1117
1118        ## if we don't have a key exit
1119        if not key:
1120            return xchat.EAT_NONE
1121
1122        ## if the key object is there but the key deleted or marked not active...and force is not set by command like /msg+ or /notice+
1123        if key.key == None or (key.active == False and not force):
1124            return xchat.EAT_NONE
1125
1126        ## if the message is marked with the plaintextmarker (default +p) don't encrypt
1127        if message.startswith(self.config['PLAINTEXTMARKER']):
1128            ## remove the plaintextmarker from the message
1129            sendmessages = [message[len(self.config['PLAINTEXTMARKER'])+1:]]
1130            messages = sendmessages
1131        else:
1132            ## encrypt message
1133            maxlen = self.config['MAXMESSAGELENGTH']
1134            cutmsg = message
1135            if action:
1136                cutmsg = "\001ACTION %s\001" % cutmsg
1137            messages = []
1138            sendmessages = []
1139            while len(cutmsg) >0:
1140                sendmessages.append(self.encrypt(key,cutmsg[:maxlen]))
1141                messages.append(cutmsg[:maxlen])
1142                cutmsg = cutmsg[maxlen:]
1143            ## mark the nick with ° for encrypted messages
1144            nick = "%s %s" % ("°"*(1+key.cbc_mode),nick)
1145
1146        #print "DEBUG(outMsg2): %r %r %r %r" % (command,message,nick,target)
1147        for sendmsg in sendmessages:
1148            ## lock the target
1149            self.__lock_proc(True)
1150            ## send the command (PRIVMSG / NOTICE)
1151            xchat.command('%s %s :%s' % (command,target, sendmsg))
1152            ## release the lock
1153            self.__lock_proc(False)
1154
1155        for message in messages:
1156            ## if it is no notice it must be send plaintext to xchat for you
1157            if command == "PRIVMSG":
1158                if action:
1159                    self.emit_print('Channel Action',  nick, message.lstrip("\001ACTION\001").strip())
1160                else:
1161                    targetTab= xchat.find_context(channel=target)
1162                    if not targetTab and targetTab != xchat.get_context():
1163                        self.emit_print('Message Send',  "%s %s" % ("°"*(1+key.cbc_mode),target), message)
1164                    else:
1165                        self.emit_print('Your Message',  nick, message,toContext=targetTab)
1166        return xchat.EAT_ALL
1167
1168    def encrypt(self,key, msg):
1169        if key.cbc_mode:
1170            encrypt_clz = BlowfishCBC
1171            encrypt_func = mircryption_cbc_pack
1172        else:
1173            encrypt_clz = Blowfish
1174            encrypt_func = blowcrypt_pack
1175        b = encrypt_clz(key.key)
1176        return encrypt_func(msg, b)
1177
1178    ## send message to local xchat and lock it
1179    def emit_print(self,userdata,speaker,message,target=None,toContext=None):
1180        if not toContext:
1181            toContext = xchat.get_context()
1182        if userdata == None:
1183            ## if userdata is none its possible Notice
1184            userdata = "Notice"
1185        if not target:
1186            ## if no special target for the lock is set, make it the speaker
1187            target = speaker
1188        ## lock the processing of that message
1189        self.__lock_proc(True,target=target)
1190        ## check for Highlight
1191        for hl in [xchat.get_info('nick')] + xchat.get_prefs("irc_extra_hilight").split(","):
1192            if len(hl) >0 and message.find(hl) > -1:
1193                if userdata == "Channel Message":
1194                    userdata = "Channel Msg Hilight"
1195                xchat.command("GUI COLOR 3")
1196        ## send the message
1197        toContext.emit_print(userdata,speaker, message.replace('\0',''))
1198        ## release the lock
1199        self.__lock_proc(False,target=target)
1200
1201    ## set or release the lock on the processing to avoid loops
1202    def __lock_proc(self,state,target=None):
1203        ctx = xchat.get_context()
1204        if not target:
1205            ## if no target set, the current channel is the target
1206            target = ctx.get_info('channel')
1207        ## the lock is NETWORK-TARGET
1208        id = "%s-%s" % (ctx.get_info('network'),target)
1209        self.__lockMAP[id] = state
1210
1211    ## check if that message is allready processed to avoid loops
1212    def __chk_proc(self,target=None):
1213        ctx = xchat.get_context()
1214        if not target:
1215            ## if no target set, the current channel is the target
1216            target = ctx.get_info('channel')
1217        id = "%s-%s" % (ctx.get_info('network'),target)
1218        return self.__lockMAP.get(id,False)
1219
1220    # get an id from channel name and networkname
1221    def get_id(self,nick=None):
1222        ctx = xchat.get_context()
1223        if nick:
1224            target = nick
1225        else:
1226            target = str(ctx.get_info('channel'))
1227        ##return the id
1228        return (target, str(ctx.get_info('network')).lower())
1229
1230    def find_key(self,id,create=None):
1231        key = self.__KeyMap.get(id,None)
1232        target, network = id
1233        networkmap = self.__TargetMap.get(network,None)
1234        if not networkmap:
1235            networkmap = {}
1236            self.__TargetMap[network] = networkmap
1237        if not key:
1238            lastaxx,key = networkmap.get(target,(-1,None))
1239        else:
1240            for _target,_key in filter(lambda x: x[1] == key,networkmap.items()):
1241                if _target != target:
1242                    del networkmap[_target]
1243        if not key and create:
1244            key = create
1245        if key:
1246            self.__TargetMap[network][target] = (int(time.time()),key)
1247        return key
1248
1249    ## return the nick only
1250    def get_nick(self,full):
1251        if full[0] == ':':
1252            full = full[1:]
1253        try:
1254            ret = full[:full.index('!')]
1255        except ValueError:
1256            ret  = full
1257        return ret
1258
1259    ## print encrypted localy
1260    def prn_crypt(self,word, word_eol, userdata):
1261        id = self.get_id()
1262        target, network = id
1263        key = self.find_key(id)
1264        if len(word_eol) < 2:
1265            print "usage: /PRNCRYPT <msg to encrypt>"
1266        else:
1267            if key:
1268                print "%s%s" % (COLOR['blue'],self.encrypt(key,word_eol[1]))
1269            else:
1270                print "%sNo known Key found for %s" % (COLOR['red'],target,)
1271        return xchat.EAT_ALL
1272
1273    ## print decrypted localy
1274    def prn_decrypt(self,word, word_eol, userdata):
1275        id = self.get_id()
1276        target, network = id
1277        key = self.find_key(id)
1278        if len(word_eol) < 2:
1279            print "usage: /PRNDECRYPT <msg to decrypt>"
1280        else:
1281            if key:
1282                print "%s%s" % (COLOR['blue'],self.decrypt(key,word_eol[1]))
1283            else:
1284                print "%sNo known Key found for %s" % (COLOR['red'],target,)
1285        return xchat.EAT_ALL
1286
1287
1288    ## manual set a key for a nick or channel
1289    def set_key(self,word, word_eol, userdata):
1290        id = self.get_id()
1291        target, network = id
1292
1293        ## if more than 2 parameter the nick/channel target is set to para 1 and the key is para 2
1294        if len(word) > 2:
1295            target = word[1]
1296            if target.find("@") > 0:
1297                target,network = target.split("@",1)
1298            newkey = word[2]
1299            id = (target,network)
1300        ## else the current channel/nick is taken as target and the key is para 1
1301        else:
1302            newkey = word[1]
1303        if len(newkey) < 8 or len(newkey) > 56:
1304            print "Key must be between 8 and 56 chars"
1305            return xchat.EAT_ALL
1306        ## get the Keyobject if available or get a new one
1307        key = self.find_key(id,create=SecretKey(None,protectmode=self.config['DEFAULTPROTECT'],cbcmode=self.config['DEFAULTCBC']))
1308        ## set the key
1309        key.key = newkey
1310        key.keyname = id
1311        ## put it in the key dict
1312        self.__KeyMap[id] = key
1313
1314        print "Key for %s on Network %s set to %r" % ( target,network,newkey)
1315        ## save the key storage
1316        self.saveDB()
1317        return xchat.EAT_ALL
1318
1319    ## delete a key or all
1320    def del_key(self,word, word_eol, userdata):
1321        ## don't accept no parameter
1322        if len(word) <2:
1323            print "Error: /DELKEY nick|channel|* (* deletes all keys)"
1324            return xchat.EAT_ALL
1325        target = word_eol[1]
1326        ## if target name is * delete all
1327        if target == "*":
1328            self.__KeyMap = {}
1329        else:
1330            if target.find("@") > 0:
1331                target,network = target.split("@",1)
1332                id = target,network
1333            else:
1334                id = self.get_id(nick=target)
1335                target,network = id
1336            ## try to delete the key
1337            try:
1338                del self.__KeyMap[id]
1339                print "Key for %s on %s deleted" % (target,network)
1340            except KeyError:
1341                print "Key %r not found" % (id,)
1342        ## save the keystorage
1343        self.saveDB()
1344        return xchat.EAT_ALL
1345
1346    ## show either key for current chan/nick or all
1347    def show_key(self,word, word_eol, userdata):
1348        ## if no parameter show key for current chan/nick
1349        if len(word) <2:
1350            id = self.get_id()
1351        else:
1352            target = word_eol[1]
1353            network = ""
1354            if target.find("@") > 0:
1355                target,network = target.split("@",1)
1356                if network.find("*") > -1:
1357                    network = network[:-1]
1358            ## if para 1 is * show all keys and there states
1359            if target.find("*") > -1:
1360                print " -------- nick/chan ------- -------- network ------- -ON- -CBC- -PROTECT- -------------------- Key --------------------"
1361                for id,keys in self.__KeyMap.items():
1362                    if id[0].startswith(target[:-1]) and id[1].startswith(network):
1363                        print "  %-26.26s %-22.22s  %2.2s   %3.3s   %5.5s      %s" % (id[0],id[1],YESNO(keys.active),YESNO(keys.cbc_mode),YESNO(keys.protect_mode),keys.key)
1364
1365                return xchat.EAT_ALL
1366            ## else get the id for the target
1367            id = self.get_id(nick=target)
1368
1369        ## get the Key
1370        key = self.find_key(id)
1371        if key:
1372            ## show Key for the specified chan/nick
1373            print "[ %s ] Key: %s - Active: %s - CBC: %s - PROTECT: %s" % (key,key.key,YESNO(key.active),YESNO(key.cbc_mode),YESNO(key.protect_mode))
1374        else:
1375            print "No Key found"
1376        return xchat.EAT_ALL
1377
1378    ## start the DH1080 Key Exchange
1379    def key_exchange(self,word, word_eol, userdata):
1380        id = self.get_id()
1381        target,network = id
1382        if len(word) >1:
1383            target = word[1]
1384            id = (target,network)
1385
1386        ## fixme chan notice - what should happen when keyx is send to channel trillian seems to accept it and send me a key --
1387        if target.startswith("#"):
1388            print "Channel Exchange not implemented"
1389            return xchat.EAT_ALL
1390
1391        ## create DH
1392        dh = DH1080Ctx()
1393
1394        self.__KeyMap[id] = self.find_key(id,create=SecretKey(dh,protectmode=self.config['DEFAULTPROTECT'],cbcmode=self.config['DEFAULTCBC']))
1395        self.__KeyMap[id].keyname = id
1396        self.__KeyMap[id].dh = dh
1397
1398        ## lock the target
1399        self.__lock_proc(True)
1400        ## send key with notice to target
1401        xchat.command('NOTICE %s %s' % (target, dh1080_pack(dh)))
1402        ## release the lock
1403        self.__lock_proc(False)
1404
1405        ## save the key storage
1406        self.saveDB()
1407        return xchat.EAT_ALL
1408
1409
1410    ## Answer to KeyExchange
1411    def dh1080_init(self,word, word_eol, userdata):
1412        id = self.get_id(nick=self.get_nick(word[0]))
1413        target,network = id
1414        message = word_eol[3].lstrip(":+")
1415        key = self.find_key(id,create=SecretKey(None,protectmode=self.config['DEFAULTPROTECT'],cbcmode=self.config['DEFAULTCBC']))
1416        ## Protection against a new key if "/PROTECTKEY" is on for nick
1417        if key.protect_mode:
1418            print "%sKEYPROTECTION: %s on %s" % (COLOR['red'],target,network)
1419            xchat.command("notice %s %s KEYPROTECTION:%s %s" % (target,self.config['PLAINTEXTMARKER'],COLOR['red'],target))
1420            return xchat.EAT_ALL
1421
1422        ## Stealth Check
1423        if self.config['FISHSTEALTH']:
1424            print "%sSTEALTHMODE: %s tried a keyexchange on %s" % (COLOR['green'],target,network)
1425            return xchat.EAT_ALL
1426
1427        mirc_mode = False
1428        try:
1429            if word[5] == "CBC":
1430                print "mIRC CBC KeyExchange detected."
1431                message = "%s %s" % (word[3].lstrip(":+"),word[4])
1432                mirc_mode = True
1433        except IndexError:
1434            pass
1435
1436        dh = DH1080Ctx()
1437        dh1080_unpack(message, dh)
1438        key.key = dh1080_secret(dh)
1439        key.keyname = id
1440
1441        ## lock the target
1442        self.__lock_proc(True)
1443        ## send key with notice to target
1444        if mirc_mode:
1445            xchat.command('NOTICE %s %s CBC' % (target, dh1080_pack(dh)))
1446        else:
1447            xchat.command('NOTICE %s %s' % (target, dh1080_pack(dh)))
1448
1449        ## release the lock
1450        self.__lock_proc(False)
1451        self.__KeyMap[id] = key
1452        print "DH1080 Init: %s on %s" % (target,network)
1453        #print "Key set to %r" % (key.key,)
1454        fingerprint = hashlib.sha256(str(dh.public +
1455                bytes2int(dh1080_b64decode(message.split(' ', 1)[1]))))
1456        print "Key set (Authentication: %s)" %fingerprint.hexdigest()[:32]
1457        ## save key storage
1458        self.saveDB()
1459        return xchat.EAT_ALL
1460
1461    ## Answer from targets init
1462    def dh1080_finish(self,word, word_eol, userdata):
1463        id = self.get_id(nick=self.get_nick(word[0]))
1464        message = word_eol[3].lstrip(":+")
1465        target,network = id
1466        ## fixme if not explicit send to the Target the received key is discarded - chan exchange
1467        if id not in self.__KeyMap:
1468            print "Invalid DH1080 Received from %s on %s" % (target,network)
1469            return xchat.EAT_NONE
1470        key = self.__KeyMap[id]
1471        dh1080_unpack(message, key.dh)
1472        key.key = dh1080_secret(key.dh)
1473        key.keyname = id
1474        print "DH1080 Finish: %s on %s" % (target,network)
1475        #print "Key set to %r" % (key.key,)
1476        fingerprint = hashlib.sha256(str(key.dh.public +
1477                bytes2int(dh1080_b64decode(message.split(' ', 1)[1]))))
1478        print "Key set (Authentication: %s)" %fingerprint.hexdigest()[:32]
1479        ## save key storage
1480        self.saveDB()
1481        return xchat.EAT_ALL
1482
1483    ## set cbc mode or show the status
1484    def set_cbc(self,word, word_eol, userdata):
1485        ## check for parameter
1486        if len(word) >2:
1487            # if both specified first is target second is mode on/off
1488            target = word[1]
1489            mode = word[2]
1490        else:
1491            ## if no target defined target is current chan/nick
1492            target = None
1493            if len(word) >1:
1494                ## if one parameter set mode to it else show only
1495                mode = word[1]
1496
1497        id = self.get_id(nick=target)
1498        target,network = id
1499        ## check if there is a key
1500        key = self.find_key(id)
1501        if not key:
1502            print "No Key found for %r" % (target,)
1503        else:
1504            ## if no parameter show only status
1505            if len(word) == 1:
1506                print "CBC Mode is %s" % ((key.cbc_mode and "on" or "off"),)
1507            else:
1508                ## set cbc mode to on/off
1509                key.cbc_mode = bool(mode in ONMODES)
1510                print "set CBC Mode for %s to %s" % (target,(key.cbc_mode == True and "on") or "off")
1511                ## save key storage
1512                self.saveDB()
1513        return xchat.EAT_ALL
1514
1515    ## set key protection mode or show the status
1516    def set_protect(self,word, word_eol, userdata):
1517        ## check for parameter
1518        if len(word) >2:
1519            # if both specified first is target second is mode on/off
1520            target = word[1]
1521            mode = word[2]
1522        else:
1523            ## if no target defined target is current nick, channel is not allowed/possible yet
1524            target = None
1525            if len(word) >1:
1526                ## if one parameter set mode to it else show only
1527                mode = word[1]
1528
1529        id = self.get_id(nick=target)
1530        target,network = id
1531        if "#" in target:
1532            print "We don't make channel protection. Sorry!"
1533            return xchat.EAT_ALL
1534
1535        key = self.find_key(id)
1536        ## check if there is a key
1537        if not key:
1538            print "No Key found for %r" % (target,)
1539        else:
1540            ## if no parameter show only status
1541            if len(word) == 1:
1542                print "KEY Protection is %s" % ((key.protect_mode and "on" or "off"),)
1543            else:
1544                ## set KEY Protection mode to on/off
1545                key.protect_mode = bool(mode in ONMODES)
1546                print "set KEY Protection for %s to %s" % (target,(key.protect_mode == True and "on") or "off")
1547                ## save key storage
1548                self.saveDB()
1549        return xchat.EAT_ALL
1550
1551
1552    ## activate/deactivate encryption for chan/nick
1553    def set_act(self,word, word_eol, userdata):
1554        ## if two parameter first is target second is mode on/off
1555        if len(word) >2:
1556            target = word[1]
1557            mode = word[2]
1558        else:
1559            ## target is current chan/nick
1560            target = None
1561            if len(word) >1:
1562                ## if one parameter set mode to on/off
1563                mode = word[1]
1564
1565        id = self.get_id(nick=target)
1566        target,network = id
1567        key = self.find_key(id)
1568        ## key not found
1569        if not key:
1570            print "No Key found for %r" % (target,)
1571        else:
1572            if len(word) == 1:
1573                ## show only
1574                print "Encryption is %s" % ((key.active and "on" or "off"),)
1575            else:
1576                ## set mode to on/off
1577                key.active = bool(mode in ONMODES)
1578                print "set Encryption for %s to %s" % (target,(key.active == True and "on") or "off")
1579                ## save key storage
1580                self.saveDB()
1581        return xchat.EAT_ALL
1582
1583    ## handle topic server message
1584    def server_332_topic(self,word, word_eol, userdata):
1585        ## check if allready processing
1586        if self.__chk_proc():
1587            return xchat.EAT_NONE
1588        server, cmd, nick, channel, topic = word[0], word[1], word[2], word[3], word_eol[4]
1589
1590        if word[4].endswith("+OK"):
1591            idx = len(word[4]) - len("+OK")
1592            topic = word_eol[4][idx:]
1593        elif word[4].endswith("mcps"):
1594            idx = len(word[4]) - len("mcps")
1595            topic = word_eol[4][idx:]
1596
1597        ## check if topic is crypted
1598        if not topic.startswith('+OK ') and not topic.startswith('mcps '):
1599            return xchat.EAT_NONE
1600        id = self.get_id(nick=channel)
1601        ## look for a key
1602        key = self.find_key(id,create=SecretKey(None))
1603        ## if no key exit
1604        if not key.key:
1605            return xchat.EAT_NONE
1606        ## decrypt
1607        topic = self.decrypt(key, topic)
1608        ##todo utf8 check for illegal chars
1609        if not topic:
1610            return xchat.EAT_NONE
1611        ## lock the target
1612        self.__lock_proc(True)
1613        ## send the message to xchat
1614        xchat.command('RECV %s %s %s %s :%s' % (server, cmd, nick, channel, topic.replace("\x00","")))
1615        ## release the lock
1616        self.__lock_proc(False)
1617        return xchat.EAT_ALL
1618
1619    ## trace nick changes
1620    def nick_trace(self,word, word_eol, userdata):
1621        old, new = word[0], word[1]
1622        ## create id's for old and new nick
1623        oldid,newid = (self.get_id(nick=old),self.get_id(nick=new))
1624        target, network = newid
1625        networkmap = self.__TargetMap.get(network,None)
1626        if not networkmap:
1627            networkmap = {}
1628            self.__TargetMap[network] = networkmap
1629        key = self.__KeyMap.get(oldid,None)
1630        if not key:
1631            lastaxx,key = networkmap.get(old,(-1,None))
1632        if key:
1633            ## make the new nick the entry the old
1634            networkmap[new] = (int(time.time()),key)
1635            try:
1636                del networkmap[old]
1637            except KeyError:
1638                pass
1639            ## save key storage
1640            self.saveDB()
1641        return xchat.EAT_NONE
1642
1643## Preliminaries.
1644
1645class MalformedError(Exception):
1646    pass
1647
1648
1649def sha256(s):
1650    """sha256"""
1651    return hashlib.sha256(s).digest()
1652
1653
1654def int2bytes(n):
1655    """Integer to variable length big endian."""
1656    if n == 0:
1657        return '\x00'
1658    b = []
1659    while n:
1660        b.insert(0,chr(n % 256))
1661        n /= 256
1662    return "".join(b)
1663
1664
1665def bytes2int(b):
1666    """Variable length big endian to integer."""
1667    n = 0
1668    for p in b:
1669        n *= 256
1670        n += ord(p)
1671    return n
1672
1673def padto(msg, length):
1674    """Pads 'msg' with zeroes until it's length is divisible by 'length'.
1675    If the length of msg is already a multiple of 'length', does nothing."""
1676    L = len(msg)
1677    if L % length:
1678        msg = "%s%s" % (msg,'\x00' * (length - L % length))
1679    assert len(msg) % length == 0
1680    return msg
1681
1682def cbc_encrypt(func, data, blocksize):
1683    """The CBC mode. The randomy generated IV is prefixed to the ciphertext.
1684    'func' is a function that encrypts data in ECB mode. 'data' is the
1685    plaintext. 'blocksize' is the block size of the cipher."""
1686    assert len(data) % blocksize == 0
1687
1688    IV = os.urandom(blocksize)
1689    assert len(IV) == blocksize
1690
1691    ciphertext = IV
1692    for block_index in xrange(len(data) / blocksize):
1693        xored = xorstring(data[:blocksize], IV)
1694        enc = func(xored)
1695
1696        ciphertext += enc
1697        IV = enc
1698        data = data[blocksize:]
1699
1700    assert len(ciphertext) % blocksize == 0
1701    return ciphertext
1702
1703
1704def cbc_decrypt(func, data, blocksize):
1705    """See cbc_encrypt."""
1706    assert len(data) % blocksize == 0
1707
1708    IV = data[0:blocksize]
1709    data = data[blocksize:]
1710
1711    plaintext = ''
1712    for block_index in xrange(len(data) / blocksize):
1713        temp = func(data[0:blocksize])
1714        temp2 = xorstring(temp, IV)
1715        plaintext += temp2
1716        IV = data[0:blocksize]
1717        data = data[blocksize:]
1718
1719    assert len(plaintext) % blocksize == 0
1720    return plaintext
1721
1722
1723class Blowfish:
1724    def __init__(self, key=None):
1725        if key:
1726            self.blowfish = cBlowfish.new(key)
1727
1728    def decrypt(self, data):
1729        return self.blowfish.decrypt(data)
1730
1731    def encrypt(self, data):
1732        return self.blowfish.encrypt(data)
1733
1734
1735class BlowfishCBC:
1736
1737    def __init__(self, key=None):
1738        if key:
1739            self.blowfish = cBlowfish.new(key)
1740
1741    def decrypt(self, data):
1742        return cbc_decrypt(self.blowfish.decrypt, data, 8)
1743
1744    def encrypt(self, data):
1745        return cbc_encrypt(self.blowfish.encrypt, data, 8)
1746
1747## blowcrypt, Fish etc.
1748# XXX: Unstable.
1749def blowcrypt_b64encode(s):
1750    """A non-standard base64-encode."""
1751    B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
1752    res = []
1753    while s:
1754        left, right = struct.unpack('>LL', s[:8])
1755        for i in xrange(6):
1756            res.append( B64[right & 0x3f] )
1757            right >>= 6
1758        for i in xrange(6):
1759            res.append( B64[left & 0x3f] )
1760            left >>= 6
1761        s = s[8:]
1762    return "".join(res)
1763
1764def blowcrypt_b64decode(s):
1765    """A non-standard base64-decode."""
1766    B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
1767    res = []
1768    while s:
1769        left, right = 0, 0
1770        for i, p in enumerate(s[0:6]):
1771            right |= B64.index(p) << (i * 6)
1772        for i, p in enumerate(s[6:12]):
1773            left |= B64.index(p) << (i * 6)
1774        res.append( struct.pack('>LL', left, right) )
1775        s = s[12:]
1776    return "".join(res)
1777
1778def blowcrypt_pack(msg, cipher):
1779    """."""
1780    return '+OK %s' % (blowcrypt_b64encode(cipher.encrypt(padto(msg, 8))))
1781
1782def blowcrypt_unpack(msg, cipher):
1783    """."""
1784    if not (msg.startswith('+OK ') or msg.startswith('mcps ')):
1785        raise ValueError
1786    _, rest = msg.split(' ', 1)
1787    if (len(rest) % 12):
1788        raise MalformedError
1789
1790    try:
1791        raw = blowcrypt_b64decode(rest)
1792    except TypeError:
1793        raise MalformedError
1794    if not raw:
1795        raise MalformedError
1796
1797    try:
1798        plain = cipher.decrypt(raw)
1799    except ValueError:
1800        raise MalformedError
1801
1802    return plain.strip('\x00')
1803
1804## Mircryption-CBC
1805def mircryption_cbc_pack(msg, cipher):
1806    """."""
1807    padded = padto(msg, 8)
1808    return '+OK *%s' % (base64.b64encode(cipher.encrypt(padded)))
1809
1810
1811def mircryption_cbc_unpack(msg, cipher):
1812    """."""
1813    if not (msg.startswith('+OK *') or msg.startswith('mcps *')):
1814        raise ValueError
1815
1816    try:
1817        _, coded = msg.split('*', 1)
1818        raw = base64.b64decode(coded)
1819    except TypeError:
1820        raise MalformedError
1821    if not raw:
1822        raise MalformedError
1823
1824    try:
1825        padded = cipher.decrypt(raw)
1826    except ValueError:
1827        raise MalformedError
1828    if not padded:
1829        raise MalformedError
1830
1831    return padded.strip('\x00')
1832
1833## DH1080
1834g_dh1080 = 2
1835p_dh1080 = int('FBE1022E23D213E8ACFA9AE8B9DFAD'
1836               'A3EA6B7AC7A7B7E95AB5EB2DF85892'
1837               '1FEADE95E6AC7BE7DE6ADBAB8A783E'
1838               '7AF7A7FA6A2B7BEB1E72EAE2B72F9F'
1839               'A2BFB2A2EFBEFAC868BADB3E828FA8'
1840               'BADFADA3E4CC1BE7E8AFE85E9698A7'
1841               '83EB68FA07A77AB6AD7BEB618ACF9C'
1842               'A2897EB28A6189EFA07AB99A8A7FA9'
1843               'AE299EFA7BA66DEAFEFBEFBF0B7D8B', 16)
1844q_dh1080 = (p_dh1080 - 1) / 2
1845
1846def dh1080_b64encode(s):
1847    """A non-standard base64-encode."""
1848    b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
1849    d = [0]*len(s)*2
1850
1851    L = len(s) * 8
1852    m = 0x80
1853    i, j, k, t = 0, 0, 0, 0
1854    while i < L:
1855        if ord(s[i >> 3]) & m:
1856            t |= 1
1857        j += 1
1858        m >>= 1
1859        if not m:
1860            m = 0x80
1861        if not j % 6:
1862            d[k] = b64[t]
1863            t &= 0
1864            k += 1
1865        t <<= 1
1866        t %= 0x100
1867        #
1868        i += 1
1869    m = 5 - j % 6
1870    t <<= m
1871    t %= 0x100
1872    if m:
1873        d[k] = b64[t]
1874        k += 1
1875    d[k] = 0
1876    res = []
1877    for q in d:
1878        if q == 0:
1879            break
1880        res.append(q)
1881    return "".join(res)
1882
1883def dh1080_b64decode(s):
1884    """A non-standard base64-encode."""
1885    b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
1886    buf = [0]*256
1887    for i in range(64):
1888        buf[ord(b64[i])] = i
1889
1890    L = len(s)
1891    if L < 2:
1892        raise ValueError
1893    for i in reversed(range(L-1)):
1894        if buf[ord(s[i])] == 0:
1895            L -= 1
1896        else:
1897            break
1898    if L < 2:
1899        raise ValueError
1900
1901    d = [0]*L
1902    i, k = 0, 0
1903    while True:
1904        i += 1
1905        if k + 1 < L:
1906            d[i-1] = buf[ord(s[k])] << 2
1907            d[i-1] %= 0x100
1908        else:
1909            break
1910        k += 1
1911        if k < L:
1912            d[i-1] |= buf[ord(s[k])] >> 4
1913        else:
1914            break
1915        i += 1
1916        if k + 1 < L:
1917            d[i-1] = buf[ord(s[k])] << 4
1918            d[i-1] %= 0x100
1919        else:
1920            break
1921        k += 1
1922        if k < L:
1923            d[i-1] |= buf[ord(s[k])] >> 2
1924        else:
1925            break
1926        i += 1
1927        if k + 1 < L:
1928            d[i-1] = buf[ord(s[k])] << 6
1929            d[i-1] %= 0x100
1930        else:
1931            break
1932        k += 1
1933        if k < L:
1934            d[i-1] |= buf[ord(s[k])] % 0x100
1935        else:
1936            break
1937        k += 1
1938    return ''.join(map(chr, d[0:i-1]))
1939
1940
1941def dh_validate_public(public, q, p):
1942    """See RFC 2631 section 2.1.5."""
1943    return 1 == pow(public, q, p)
1944
1945
1946class DH1080Ctx:
1947    """DH1080 context."""
1948    def __init__(self):
1949        self.public = 0
1950        self.private = 0
1951        self.secret = 0
1952        self.state = 0
1953
1954        bits = 1080
1955        while True:
1956            self.private = bytes2int(os.urandom(bits/8))
1957            self.public = pow(g_dh1080, self.private, p_dh1080)
1958            if 2 <= self.public <= p_dh1080 - 1 and \
1959               dh_validate_public(self.public, q_dh1080, p_dh1080) == 1:
1960                break
1961
1962def dh1080_pack(ctx):
1963    """."""
1964    if ctx.state == 0:
1965        ctx.state = 1
1966        cmd = "DH1080_INIT"
1967    else:
1968        cmd = "DH1080_FINISH"
1969    return "%s %s" % (cmd,dh1080_b64encode(int2bytes(ctx.public)))
1970
1971def dh1080_unpack(msg, ctx):
1972    """."""
1973    if not "DH1080_" in msg:
1974        raise ValueError
1975
1976    invalidmsg = "Key does not validate per RFC 2631. This check is not performed by any DH1080 implementation, so we use the key anyway. See RFC 2785 for more details."
1977
1978    if ctx.state == 0:
1979        if not "DH1080_INIT " in msg:
1980            raise MalformedError
1981        ctx.state = 1
1982        try:
1983            cmd, public_raw = msg.split(' ', 1)
1984            public = bytes2int(dh1080_b64decode(public_raw))
1985
1986            if not 1 < public < p_dh1080:
1987                raise MalformedError
1988
1989            if not dh_validate_public(public, q_dh1080, p_dh1080):
1990                print invalidmsg
1991                pass
1992
1993            ctx.secret = pow(public, ctx.private, p_dh1080)
1994        except:
1995            raise MalformedError
1996
1997    elif ctx.state == 1:
1998        if not "DH1080_FINISH " in msg:
1999            raise MalformedError
2000        ctx.state = 1
2001        try:
2002            cmd, public_raw = msg.split(' ', 1)
2003            public = bytes2int(dh1080_b64decode(public_raw))
2004
2005            if not 1 < public < p_dh1080:
2006                raise MalformedError
2007
2008            if not dh_validate_public(public, q_dh1080, p_dh1080):
2009                print invalidmsg
2010                pass
2011
2012            ctx.secret = pow(public, ctx.private, p_dh1080)
2013        except:
2014            raise MalformedError
2015
2016    return True
2017
2018
2019def dh1080_secret(ctx):
2020    """."""
2021    if ctx.secret == 0:
2022        raise ValueError
2023    return dh1080_b64encode(sha256(int2bytes(ctx.secret)))
2024
2025if REQUIRESETUP:
2026    __install_thread = None
2027    def _install_pyBlowfish(urllib2,doExtra):
2028        global __install_thread
2029        if __install_thread:
2030            print "Install is allready running"
2031            return
2032        __install_thread = Thread(target=__install_pyBlowfish,kwargs={'urllib2':urllib2,'context':xchat.get_context()})
2033        __install_thread.start()
2034    def __install_pyBlowfish(urllib2,context):
2035        global __install_thread
2036        context.prnt("\0038.....checking for pyBlowfish.py at %r... please wait ...." % PYBLOWFISHURL)
2037        try:
2038            __script = urllib2.urlopen(PYBLOWFISHURL,timeout=40).read()
2039            try:
2040                __fd = open("%s%spyBlowfish.py" % (path,sep),"wb")
2041                __fd.write(__script)
2042                print "\002\0033Please type /py reload %s" % script
2043            finally:
2044                __fd.close()
2045        except urllib2.URLError,err:
2046            print err
2047
2048        except:
2049            context.prnt( "\002\0034INSTALL FAILED" )
2050            raise
2051        __install_thread = None
2052
2053    def fishsetup(word, word_eol, userdata):
2054        useproxy = True
2055        if len(word) >1:
2056            if word[1].lower() == "noproxy":
2057                useproxy = False
2058        proxyload(_install_pyBlowfish,useproxy,None)
2059        return xchat.EAT_ALL
2060    xchat.hook_command('FISHSETUP', fishsetup)
2061else:
2062    loadObj = XChatCrypt()
2063
2064# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
2065
2066